Merge pull request #3801 from cappyzawa/feature/get-team

Enable to print specified team config (fly get-team)
This commit is contained in:
Alex Suraci 2019-05-09 17:08:22 -04:00 committed by GitHub
commit 431efe4874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 541 additions and 0 deletions

View File

@ -209,6 +209,7 @@ var requiredRoles = map[string]string{
atc.ListDestroyingVolumes: "viewer",
atc.ReportWorkerVolumes: "member",
atc.ListTeams: "viewer",
atc.GetTeam: "viewer",
atc.SetTeam: "owner",
atc.RenameTeam: "owner",
atc.DestroyTeam: "owner",

View File

@ -760,6 +760,10 @@ var _ = Describe("Accessor", func() {
Entry("pipeline-operator :: "+atc.ListTeams, atc.ListTeams, "pipeline-operator", true),
Entry("viewer :: "+atc.ListTeams, atc.ListTeams, "viewer", true),
Entry("owner :: "+atc.GetTeam, atc.GetTeam, "owner", true),
Entry("member :: "+atc.GetTeam, atc.GetTeam, "member", true),
Entry("viewer :: "+atc.GetTeam, atc.GetTeam, "viewer", true),
Entry("owner :: "+atc.SetTeam, atc.SetTeam, "owner", true),
Entry("member :: "+atc.SetTeam, atc.SetTeam, "member", false),
Entry("pipeline-operator :: "+atc.SetTeam, atc.SetTeam, "pipeline-operator", false),

View File

@ -185,6 +185,7 @@ func NewHandler(
atc.ReportWorkerVolumes: http.HandlerFunc(volumesServer.ReportWorkerVolumes),
atc.ListTeams: http.HandlerFunc(teamServer.ListTeams),
atc.GetTeam: http.HandlerFunc(teamServer.GetTeam),
atc.SetTeam: http.HandlerFunc(teamServer.SetTeam),
atc.RenameTeam: http.HandlerFunc(teamServer.RenameTeam),
atc.DestroyTeam: http.HandlerFunc(teamServer.DestroyTeam),

View File

@ -166,6 +166,125 @@ var _ = Describe("Teams API", func() {
})
})
Describe("GET /api/v1/teams/:team_name", func() {
var response *http.Response
var fakeTeam *dbfakes.FakeTeam
BeforeEach(func() {
fakeTeam = new(dbfakes.FakeTeam)
fakeTeam.IDReturns(1)
fakeTeam.NameReturns("a-team")
fakeTeam.AuthReturns(atc.TeamAuth{
"owner": map[string][]string{
"groups": {}, "users": {"local:username"},
},
})
})
JustBeforeEach(func() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/teams/a-team", server.URL), nil)
Expect(err).NotTo(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
response, err = client.Do(req)
Expect(err).NotTo(HaveOccurred())
})
Context("when not authenticated and not admin", func() {
BeforeEach(func() {
fakeaccess.IsAuthorizedReturns(false)
fakeaccess.IsAdminReturns(false)
})
It("returns 401", func() {
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
})
})
Context("when not authenticated to specified team, but have admin authority", func() {
BeforeEach(func() {
dbTeamFactory.FindTeamReturns(fakeTeam, true, nil)
fakeaccess.IsAuthenticatedReturns(true)
fakeaccess.IsAdminReturns(true)
fakeaccess.IsAuthorizedReturns(false)
})
It("returns 200 ok", func() {
Expect(response.StatusCode).To(Equal(http.StatusOK))
})
It("returns application/json", func() {
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
})
It("returns a team JSON", func() {
body, err := ioutil.ReadAll(response.Body)
Expect(err).NotTo(HaveOccurred())
Expect(body).To(MatchJSON(`
{
"id": 1,
"name": "a-team",
"auth": {
"owner": {
"groups": [],
"users": [
"local:username"
]
}
}
}`))
})
})
Context("when authenticated to specified team", func() {
BeforeEach(func() {
dbTeamFactory.FindTeamReturns(fakeTeam, true, nil)
fakeaccess.IsAuthenticatedReturns(true)
fakeaccess.IsAdminReturns(false)
fakeaccess.IsAuthorizedReturns(true)
})
It("returns 200 ok", func() {
Expect(response.StatusCode).To(Equal(http.StatusOK))
})
It("returns application/json", func() {
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
})
It("returns a team JSON", func() {
body, err := ioutil.ReadAll(response.Body)
Expect(err).NotTo(HaveOccurred())
Expect(body).To(MatchJSON(`
{
"id": 1,
"name": "a-team",
"auth": {
"owner": {
"groups": [],
"users": [
"local:username"
]
}
}
}`))
})
})
Context("when authenticated as another team", func() {
BeforeEach(func() {
dbTeamFactory.FindTeamReturns(fakeTeam, true, nil)
fakeaccess.IsAuthenticatedReturns(true)
})
It("return 401", func() {
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
})
})
})
Describe("PUT /api/v1/teams/:team_name", func() {
var (
response *http.Response

49
atc/api/teamserver/get.go Normal file
View File

@ -0,0 +1,49 @@
package teamserver
import (
"encoding/json"
"errors"
"net/http"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/api/accessor"
"github.com/concourse/concourse/atc/api/present"
)
func (s *Server) GetTeam(w http.ResponseWriter, r *http.Request) {
hLog := s.logger.Session("get-team")
hLog.Debug("getting-team")
teamName := r.FormValue(":team_name")
team, found, err := s.teamFactory.FindTeam(teamName)
if err != nil {
hLog.Error("failed-to-get-team", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if !found {
w.WriteHeader(http.StatusNotFound)
return
}
acc := accessor.GetAccessor(r)
var presentedTeam atc.Team
if acc.IsAdmin() || acc.IsAuthorized(team.Name()) {
presentedTeam = present.Team(team)
} else {
hLog.Error("unauthorized", errors.New("not authorized to "+team.Name()))
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(presentedTeam); err != nil {
hLog.Error("failed-to-encode-team", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}

View File

@ -91,6 +91,7 @@ const (
ReportWorkerVolumes = "ReportWorkerVolumes"
ListTeams = "ListTeams"
GetTeam = "GetTeam"
SetTeam = "SetTeam"
RenameTeam = "RenameTeam"
DestroyTeam = "DestroyTeam"
@ -197,6 +198,7 @@ var Routes = rata.Routes([]rata.Route{
{Path: "/api/v1/volumes/report", Method: "PUT", Name: ReportWorkerVolumes},
{Path: "/api/v1/teams", Method: "GET", Name: ListTeams},
{Path: "/api/v1/teams/:team_name", Method: "GET", Name: GetTeam},
{Path: "/api/v1/teams/:team_name", Method: "PUT", Name: SetTeam},
{Path: "/api/v1/teams/:team_name/rename", Method: "PUT", Name: RenameTeam},
{Path: "/api/v1/teams/:team_name", Method: "DELETE", Name: DestroyTeam},

View File

@ -102,6 +102,7 @@ func (wrappa *APIAuthWrappa) Wrap(handlers rata.Handlers) rata.Handlers {
atc.RegisterWorker,
atc.HeartbeatWorker,
atc.DeleteWorker,
atc.GetTeam,
atc.SetTeam,
atc.ListTeamBuilds,
atc.RenameTeam,

View File

@ -202,6 +202,7 @@ var _ = Describe("APIAuthWrappa", func() {
atc.RegisterWorker: authenticated(inputHandlers[atc.RegisterWorker]),
atc.HeartbeatWorker: authenticated(inputHandlers[atc.HeartbeatWorker]),
atc.DeleteWorker: authenticated(inputHandlers[atc.DeleteWorker]),
atc.GetTeam: authenticated(inputHandlers[atc.GetTeam]),
atc.SetTeam: authenticated(inputHandlers[atc.SetTeam]),
atc.RenameTeam: authenticated(inputHandlers[atc.RenameTeam]),
atc.DestroyTeam: authenticated(inputHandlers[atc.DestroyTeam]),

View File

@ -24,6 +24,7 @@ type FlyCommand struct {
Userinfo UserinfoCommand `command:"userinfo" description:"User information"`
Teams TeamsCommand `command:"teams" alias:"t" description:"List the configured teams"`
GetTeam GetTeamCommand `command:"get-team" alias:"gt" description:"Show team configuration"`
SetTeam SetTeamCommand `command:"set-team" alias:"st" description:"Create or modify a team to have the given credentials"`
RenameTeam RenameTeamCommand `command:"rename-team" alias:"rt" description:"Rename a team"`
DestroyTeam DestroyTeamCommand `command:"destroy-team" alias:"dt" description:"Destroy a team and delete all of its data"`

84
fly/commands/get_team.go Normal file
View File

@ -0,0 +1,84 @@
package commands
import (
"errors"
"fmt"
"os"
"sort"
"strings"
"github.com/concourse/concourse/fly/commands/internal/displayhelpers"
"github.com/concourse/concourse/fly/rc"
"github.com/concourse/concourse/fly/ui"
"github.com/fatih/color"
)
type GetTeamCommand struct {
Team string `short:"n" long:"team" required:"true" description:"Get configuration of this team"`
Json bool `short:"j" long:"json" description:"Print config as json instead of yaml"`
}
func (command *GetTeamCommand) Execute(args []string) error {
target, err := rc.LoadTarget(Fly.Target, Fly.Verbose)
if err != nil {
return err
}
if err := target.Validate(); err != nil {
return err
}
team, found, err := target.Team().Team(command.Team)
if err != nil {
return err
}
if !found {
return errors.New("team not found")
}
if command.Json {
if err := displayhelpers.JsonPrint(team); err != nil {
return err
}
return nil
}
headers := ui.TableRow{
{Contents: "name/role", Color: color.New(color.Bold)},
{Contents: "users", Color: color.New(color.Bold)},
{Contents: "groups", Color: color.New(color.Bold)},
}
table := ui.Table{Headers: headers}
for role, auth := range team.Auth {
row := ui.TableRow{
{Contents: fmt.Sprintf("%s/%s", team.Name, role)},
}
var usersCell, groupsCell ui.TableCell
hasUsers := len(auth["users"]) != 0
hasGroups := len(auth["groups"]) != 0
if !hasUsers && !hasGroups {
usersCell.Contents = "all"
usersCell.Color = color.New(color.Faint)
} else if !hasUsers {
usersCell.Contents = "none"
usersCell.Color = color.New(color.Faint)
} else {
usersCell.Contents = strings.Join(auth["users"], ",")
}
if hasGroups {
groupsCell.Contents = strings.Join(auth["groups"], ",")
} else {
groupsCell.Contents = "none"
groupsCell.Color = color.New(color.Faint)
}
row = append(row, usersCell)
row = append(row, groupsCell)
table.Data = append(table.Data, row)
}
sort.Sort(table.Data)
return table.Render(os.Stdout, Fly.PrintTableHeaders)
}

View File

@ -0,0 +1,104 @@
package integration_test
import (
"net/http"
"os/exec"
"github.com/concourse/concourse/atc"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
"github.com/onsi/gomega/ghttp"
"github.com/tedsuo/rata"
"gopkg.in/yaml.v2"
)
var _ = Describe("GetPipeline", func() {
var (
team atc.Team
)
BeforeEach(func() {
team = atc.Team{
ID: 1,
Name: "myTeam",
Auth: atc.TeamAuth{
"owner": map[string][]string{
"groups": {}, "users": {"local:username"},
},
},
}
Context("when not specifying a team name", func() {
It("fails and says you should give a team name", func() {
flyCmd := exec.Command(flyPath, "-t", targetName, "get-team")
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
<-sess.Exited
Expect(sess.ExitCode()).To(Equal(1))
Expect(sess.Err).To(gbytes.Say("error: the required flag `" + osFlag("n", "team") + "' was not specified"))
})
})
Context("when specifying a team name", func() {
var path string
BeforeEach(func() {
var err error
path, err = atc.Routes.CreatePathForRoute(atc.GetTeam, rata.Params{"team_name": "myTeam"})
Expect(err).NotTo(HaveOccurred())
})
Context("and team is not found", func() {
JustBeforeEach(func() {
atcServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", path),
ghttp.RespondWithJSONEncoded(http.StatusNotFound, ""),
),
)
})
It("should print team not found error", func() {
flyCmd := exec.Command(flyPath, "-t", targetName, "get-team", "-n", "myTeam")
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
<-sess.Exited
Expect(sess.ExitCode()).To(Equal(1))
})
})
Context("when atc returns team config", func() {
BeforeEach(func() {
atcServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", path),
ghttp.RespondWithJSONEncoded(200, team),
),
)
})
It("prints the config as yaml to stdout", func() {
flyCmd := exec.Command(flyPath, "-t", targetName, "get-team", "myTeam")
sess, err := gexec.Start(flyCmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
<-sess.Exited
Expect(sess.ExitCode()).To(Equal(0))
var printedConfig atc.Team
err = yaml.Unmarshal(sess.Out.Contents(), &printedConfig)
Expect(err).NotTo(HaveOccurred())
Expect(printedConfig).To(Equal(team))
})
})
})
})
})

View File

@ -594,6 +594,21 @@ type FakeTeam struct {
result3 bool
result4 error
}
TeamStub func(string) (atc.Team, bool, error)
teamMutex sync.RWMutex
teamArgsForCall []struct {
arg1 string
}
teamReturns struct {
result1 atc.Team
result2 bool
result3 error
}
teamReturnsOnCall map[int]struct {
result1 atc.Team
result2 bool
result3 error
}
UnpauseJobStub func(string, string) (bool, error)
unpauseJobMutex sync.RWMutex
unpauseJobArgsForCall []struct {
@ -3230,6 +3245,72 @@ func (fake *FakeTeam) ResourceVersionsReturnsOnCall(i int, result1 []atc.Resourc
}{result1, result2, result3, result4}
}
func (fake *FakeTeam) Team(arg1 string) (atc.Team, bool, error) {
fake.teamMutex.Lock()
ret, specificReturn := fake.teamReturnsOnCall[len(fake.teamArgsForCall)]
fake.teamArgsForCall = append(fake.teamArgsForCall, struct {
arg1 string
}{arg1})
fake.recordInvocation("Team", []interface{}{arg1})
fake.teamMutex.Unlock()
if fake.TeamStub != nil {
return fake.TeamStub(arg1)
}
if specificReturn {
return ret.result1, ret.result2, ret.result3
}
fakeReturns := fake.teamReturns
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3
}
func (fake *FakeTeam) TeamCallCount() int {
fake.teamMutex.RLock()
defer fake.teamMutex.RUnlock()
return len(fake.teamArgsForCall)
}
func (fake *FakeTeam) TeamCalls(stub func(string) (atc.Team, bool, error)) {
fake.teamMutex.Lock()
defer fake.teamMutex.Unlock()
fake.TeamStub = stub
}
func (fake *FakeTeam) TeamArgsForCall(i int) string {
fake.teamMutex.RLock()
defer fake.teamMutex.RUnlock()
argsForCall := fake.teamArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeTeam) TeamReturns(result1 atc.Team, result2 bool, result3 error) {
fake.teamMutex.Lock()
defer fake.teamMutex.Unlock()
fake.TeamStub = nil
fake.teamReturns = struct {
result1 atc.Team
result2 bool
result3 error
}{result1, result2, result3}
}
func (fake *FakeTeam) TeamReturnsOnCall(i int, result1 atc.Team, result2 bool, result3 error) {
fake.teamMutex.Lock()
defer fake.teamMutex.Unlock()
fake.TeamStub = nil
if fake.teamReturnsOnCall == nil {
fake.teamReturnsOnCall = make(map[int]struct {
result1 atc.Team
result2 bool
result3 error
})
}
fake.teamReturnsOnCall[i] = struct {
result1 atc.Team
result2 bool
result3 error
}{result1, result2, result3}
}
func (fake *FakeTeam) UnpauseJob(arg1 string, arg2 string) (bool, error) {
fake.unpauseJobMutex.Lock()
ret, specificReturn := fake.unpauseJobReturnsOnCall[len(fake.unpauseJobArgsForCall)]
@ -3506,6 +3587,8 @@ func (fake *FakeTeam) Invocations() map[string][][]interface{} {
defer fake.resourceMutex.RUnlock()
fake.resourceVersionsMutex.RLock()
defer fake.resourceVersionsMutex.RUnlock()
fake.teamMutex.RLock()
defer fake.teamMutex.RUnlock()
fake.unpauseJobMutex.RLock()
defer fake.unpauseJobMutex.RUnlock()
fake.unpausePipelineMutex.RLock()

View File

@ -12,6 +12,7 @@ import (
type Team interface {
Name() string
Team(teamName string) (atc.Team, bool, error)
CreateOrUpdate(team atc.Team) (atc.Team, bool, bool, error)
RenameTeam(teamName, name string) (bool, error)
DestroyTeam(teamName string) error

View File

@ -14,6 +14,27 @@ import (
var ErrDestroyRefused = errors.New("not-permitted-to-destroy-as-requested")
// Team get team with the name given as argument.
func (team *team) Team(teamName string) (atc.Team, bool, error) {
var t atc.Team
params := rata.Params{"team_name": teamName}
err := team.connection.Send(internal.Request{
RequestName: atc.GetTeam,
Params: params,
}, &internal.Response{
Result: &t,
})
switch err.(type) {
case nil:
return t, true, nil
case internal.ResourceNotFoundError:
return atc.Team{}, false, nil
default:
return atc.Team{}, false, err
}
}
// CreateOrUpdate creates or updates team teamName with the settings provided in passedTeam.
// passedTeam should reflect the desired state of team's configuration.
func (team *team) CreateOrUpdate(passedTeam atc.Team) (atc.Team, bool, bool, error) {

View File

@ -12,6 +12,75 @@ import (
)
var _ = Describe("ATC Handler Teams", func() {
Describe("Team", func() {
var expectedTeam atc.Team
teamName := "myTeam"
expectedURL := "/api/v1/teams/myTeam"
BeforeEach(func() {
expectedTeam = atc.Team{
ID: 1,
Name: "myTeam",
Auth: atc.TeamAuth{
"owner": map[string][]string{
"groups": {}, "users": {"local:username"},
},
},
}
})
Context("when the team is found", func() {
BeforeEach(func() {
atcServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", expectedURL),
ghttp.RespondWithJSONEncoded(http.StatusOK, expectedTeam),
),
)
})
It("returns the requested team", func() {
team, found, err := team.Team(teamName)
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
Expect(team).To(Equal(expectedTeam))
})
})
Context("when the team is not found", func() {
BeforeEach(func() {
atcServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", expectedURL),
ghttp.RespondWith(http.StatusNotFound, ""),
),
)
})
It("returns false", func() {
_, found, err := team.Team(teamName)
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeFalse())
})
})
Context("when not belonging to the team", func() {
BeforeEach(func() {
atcServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", expectedURL),
ghttp.RespondWith(http.StatusUnauthorized, ""),
),
)
})
It("returns false and error", func() {
_, found, err := team.Team(teamName)
Expect(found).To(BeFalse())
Expect(err).To(HaveOccurred())
})
})
})
Describe("CreateOrUpdate", func() {
var expectedURL = "/api/v1/teams/team venture"
var expectedTeam, desiredTeam atc.Team