Skip to content

Commit

Permalink
Expose GitHub team ID (#153)
Browse files Browse the repository at this point in the history
The team creation API for GitHub teams only requires a `githubTeamID`
parameter, which it uses to fetch all the other details like team name,
description, members, etc.

I implemented `Read` while I was in here, because it was surprising to
see this break `refresh` due to a not-implemented error. Also implemented
`Check` because `name` and `githubTeamId` are only required for certain
team types.

Tested and confirmed working by with the following program:

```typescript
import * as pulumi from "@pulumi/pulumi";
import * as service from "@pulumi/pulumiservice";

const team = new service.Team("github-team", {
  organizationName: "pulumi",
  teamType: "github",
  githubTeamId: 7475955,
});
```

Fixes #151.
  • Loading branch information
blampe authored Jul 19, 2023
1 parent 0b8c2be commit dd5e1dd
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 114 deletions.
18 changes: 13 additions & 5 deletions provider/cmd/pulumi-resource-pulumiservice/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@
"type": "string"
},
"name": {
"description": "The team name.",
"description": "The team's name. Required for \"pulumi\" teams.",
"type": "string"
},
"displayName": {
Expand All @@ -473,8 +473,12 @@
}
},
"organizationName": {
"description": "The organization's name.",
"description": "The name of the Pulumi organization the team belongs to.",
"type": "string"
},
"githubTeamId": {
"description": "The GitHub ID of the team to mirror. Must be in the same GitHub organization that the Pulumi org is backed by. Required for \"github\" teams.",
"type": "number"
}
},
"inputProperties": {
Expand All @@ -483,7 +487,7 @@
"type": "string"
},
"name": {
"description": "The team name.",
"description": "The team's name. Required for \"pulumi\" teams.",
"type": "string"
},
"displayName": {
Expand All @@ -502,11 +506,15 @@
}
},
"organizationName": {
"description": "The organization's name.",
"description": "The name of the Pulumi organization the team belongs to.",
"type": "string"
},
"githubTeamId": {
"description": "The GitHub ID of the team to mirror. Must be in the same GitHub organization that the Pulumi org is backed by. Required for \"github\" teams.",
"type": "number"
}
},
"requiredInputs": ["name", "organizationName", "teamType"]
"requiredInputs": ["organizationName", "teamType"]
},
"pulumiservice:index:TeamAccessToken": {
"description": "The Pulumi Cloud allows users to create access tokens scoped to team. Team access tokens is a resource to create them and assign them to a team",
Expand Down
29 changes: 12 additions & 17 deletions provider/pkg/internal/pulumiapi/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
Expand Down Expand Up @@ -46,6 +46,7 @@ type createTeamRequest struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Description string `json:"description"`
GitHubTeamID int64 `json:"githubTeamID,omitempty"`
}

type updateTeamRequest struct {
Expand Down Expand Up @@ -86,7 +87,6 @@ func (c *Client) ListTeams(ctx context.Context, orgName string) ([]Team, error)

var teamArray Teams
_, err := c.do(ctx, http.MethodGet, apiUrl, nil, &teamArray)

if err != nil {
return nil, fmt.Errorf("failed to list teams for %q: %w", orgName, err)
}
Expand All @@ -113,22 +113,22 @@ func (c *Client) GetTeam(ctx context.Context, orgName string, teamName string) (
return &team, nil
}

func (c *Client) CreateTeam(ctx context.Context, orgName, teamName, teamType, displayName, description string) (*Team, error) {
func (c *Client) CreateTeam(ctx context.Context, orgName, teamName, teamType, displayName, description string, teamID int64) (*Team, error) {
teamtypeList := []string{"github", "pulumi"}
if !contains(teamtypeList, teamType) {
return nil, fmt.Errorf("teamtype must be one of %v, got %q", teamtypeList, teamType)
}

if len(orgName) == 0 {
return nil, errors.New("orgname must not be empty")
}

if len(teamName) == 0 {
if len(teamName) == 0 && teamType != "github" {
return nil, errors.New("teamname must not be empty")
}

if len(teamType) == 0 {
return nil, errors.New("teamtype must not be empty")
}

teamtypeList := []string{"github", "pulumi"}
if !contains(teamtypeList, teamType) {
return nil, fmt.Errorf("teamtype must be one of %v, got %q", teamtypeList, teamType)
if teamType == "github" && teamID == 0 {
return nil, errors.New("github teams require a githubTeamId")
}

apiPath := path.Join("orgs", orgName, "teams", teamType)
Expand All @@ -139,11 +139,11 @@ func (c *Client) CreateTeam(ctx context.Context, orgName, teamName, teamType, di
Name: teamName,
DisplayName: displayName,
Description: description,
GitHubTeamID: teamID,
}

var team Team
_, err := c.do(ctx, http.MethodPost, apiPath, createReq, &team)

if err != nil {
return nil, fmt.Errorf("failed to create team: %w", err)
}
Expand All @@ -168,15 +168,13 @@ func (c *Client) UpdateTeam(ctx context.Context, orgName, teamName, displayName,
}

_, err := c.do(ctx, "PATCH", apiPath, updateReq, nil)

if err != nil {
return fmt.Errorf("failed to update team: %w", err)
}
return nil
}

func (c *Client) DeleteTeam(ctx context.Context, orgName, teamName string) error {

if len(orgName) == 0 {
return errors.New("orgname must not be empty")
}
Expand Down Expand Up @@ -220,7 +218,6 @@ func (c *Client) updateTeamMembership(ctx context.Context, orgName, teamName, us
}

_, err := c.do(ctx, http.MethodPatch, apiPath, updateMembershipReq, nil)

if err != nil {
return fmt.Errorf("failed to update team membership: %w", err)
}
Expand Down Expand Up @@ -266,7 +263,6 @@ func (c *Client) AddStackPermission(ctx context.Context, stack StackName, teamNa
}

_, err := c.do(ctx, http.MethodPatch, apiPath, addStackPermissionRequest, nil)

if err != nil {
return fmt.Errorf("failed to add stack permission for team: %w", err)
}
Expand All @@ -289,7 +285,6 @@ func (c *Client) RemoveStackPermission(ctx context.Context, stack StackName, tea
}

_, err := c.do(ctx, http.MethodPatch, apiPath, removeStackPermissionRequest, nil)

if err != nil {
return fmt.Errorf("failed to remove stack permission for team: %w", err)
}
Expand Down
77 changes: 61 additions & 16 deletions provider/pkg/internal/pulumiapi/teams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,41 +98,63 @@ func TestGetTeam(t *testing.T) {
func TestCreateTeam(t *testing.T) {
orgName := "an-organization"
teamName := "a-team"
teamType := "pulumi"
displayName := "A Team"
description := "The A Team"
team := Team{
Type: teamType,
Name: teamName,
DisplayName: displayName,
Description: description,
}
t.Run("Happy Path", func(t *testing.T) {
t.Run("Happy Path (pulumi team)", func(t *testing.T) {
expected := Team{
Type: "pulumi",
Name: teamName,
DisplayName: displayName,
Description: description,
}
c, cleanup := startTestServer(t, testServerConfig{
ExpectedReqMethod: http.MethodPost,
ExpectedReqPath: "/api/orgs/an-organization/teams/pulumi",
ExpectedReqBody: createTeamRequest{
Organization: orgName,
TeamType: teamType,
TeamType: "pulumi",
Name: teamName,
DisplayName: displayName,
Description: description,
},
ResponseBody: team,
ResponseBody: expected,
ResponseCode: 201,
})
defer cleanup()
actualTeam, err := c.CreateTeam(ctx, orgName, teamName, teamType, displayName, description)
actualTeam, err := c.CreateTeam(ctx, orgName, teamName, "pulumi", displayName, description, 0)
assert.NoError(t, err)
assert.Equal(t, team, *actualTeam)
assert.Equal(t, expected, *actualTeam)
})
t.Run("Error", func(t *testing.T) {
t.Run("Happy Path (github team)", func(t *testing.T) {
expected := Team{
Type: "github",
Name: teamName,
DisplayName: displayName,
Description: description,
}
c, cleanup := startTestServer(t, testServerConfig{
ExpectedReqMethod: http.MethodPost,
ExpectedReqPath: "/api/orgs/an-organization/teams/github",
ExpectedReqBody: createTeamRequest{
TeamType: "github",
Organization: orgName,
GitHubTeamID: 1,
},
ResponseBody: expected,
ResponseCode: 201,
})
defer cleanup()
actualTeam, err := c.CreateTeam(ctx, orgName, "", "github", "", "", 1)
assert.NoError(t, err)
assert.Equal(t, expected, *actualTeam)
})
t.Run("Error (pulumi team)", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{
ExpectedReqMethod: http.MethodPost,
ExpectedReqPath: "/api/orgs/an-organization/teams/pulumi",
ExpectedReqBody: createTeamRequest{
Organization: orgName,
TeamType: teamType,
TeamType: "pulumi",
Name: teamName,
DisplayName: displayName,
Description: description,
Expand All @@ -143,10 +165,34 @@ func TestCreateTeam(t *testing.T) {
},
})
defer cleanup()
team, err := c.CreateTeam(ctx, orgName, teamName, teamType, displayName, description)
team, err := c.CreateTeam(ctx, orgName, teamName, "pulumi", displayName, description, 0)
assert.Nil(t, team, "team should be nil since error was returned")
assert.EqualError(t, err, "failed to create team: 401 API error: unauthorized")
})
t.Run("Error (github team missing ID)", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{})
defer cleanup()
_, err := c.CreateTeam(ctx, orgName, "", "github", "", "", 0)
assert.EqualError(t, err, "github teams require a githubTeamId")
})
t.Run("Error (invalid team type)", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{})
defer cleanup()
_, err := c.CreateTeam(ctx, orgName, "", "foo", "", "", 0)
assert.EqualError(t, err, "teamtype must be one of [github pulumi], got \"foo\"")
})
t.Run("Error (invalid team name for pulumi team)", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{})
defer cleanup()
_, err := c.CreateTeam(ctx, orgName, "", "pulumi", "", "", 0)
assert.EqualError(t, err, "teamname must not be empty")
})
t.Run("Error (missing org name)", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{})
defer cleanup()
_, err := c.CreateTeam(ctx, "", "", "pulumi", "", "", 0)
assert.EqualError(t, err, "orgname must not be empty")
})
}

func TestAddMemberToTeam(t *testing.T) {
Expand Down Expand Up @@ -214,7 +260,6 @@ func TestAddStackPermission(t *testing.T) {
})
}


func TestRemoveStackPermission(t *testing.T) {
teamName := "a-team"
stack := StackName{
Expand Down
Loading

0 comments on commit dd5e1dd

Please sign in to comment.