From af0aab13deac131a1f0302b15d2f7d126a64f86b Mon Sep 17 00:00:00 2001 From: Komal Date: Mon, 18 Dec 2023 10:48:09 -0800 Subject: [PATCH] Support `Read` for TeamStackPermission (#205) Fixes #202 --- CHANGELOG.md | 2 + CHANGELOG_PENDING.md | 6 ++ examples/examples_yaml_test.go | 3 +- provider/pkg/internal/pulumiapi/teams.go | 49 ++++++++++++-- provider/pkg/provider/team_stack_perm.go | 81 ++++++++++++++++++++++-- 5 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG_PENDING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c4df4750 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +CHANGELOG +========= diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md new file mode 100644 index 00000000..62a06b68 --- /dev/null +++ b/CHANGELOG_PENDING.md @@ -0,0 +1,6 @@ +### Improvements + +### Bug Fixes + +- Fix `Read` for TeamStackPermission so resources are not deleted from state on refresh. Note: TeamStackPermission resources created before v0.17.0 will now return an error if attempting a refresh, but those (re)created with the new version will support `refresh`. [#205](https://github.com/pulumi/pulumi-pulumiservice/pull/205) + diff --git a/examples/examples_yaml_test.go b/examples/examples_yaml_test.go index 6a9c651b..dc9135de 100644 --- a/examples/examples_yaml_test.go +++ b/examples/examples_yaml_test.go @@ -219,8 +219,7 @@ func TestYamlOrgAccessTokenExample(t *testing.T) { func TestYamlTeamStackPermissionsExample(t *testing.T) { cwd, _ := os.Getwd() integration.ProgramTest(t, &integration.ProgramTestOptions{ - Quick: true, - SkipRefresh: true, + Quick: true, // Name is specified in yaml-team-stack-permissions/Pulumi.yaml, so this has to be consistent StackName: "dev", Dir: path.Join(cwd, ".", "yaml-team-stack-permissions"), diff --git a/provider/pkg/internal/pulumiapi/teams.go b/provider/pkg/internal/pulumiapi/teams.go index 8ecfe6ed..1d341318 100644 --- a/provider/pkg/internal/pulumiapi/teams.go +++ b/provider/pkg/internal/pulumiapi/teams.go @@ -38,11 +38,13 @@ type Teams struct { } type Team struct { - Type string `json:"kind"` - Name string - DisplayName string - Description string - Members []TeamMember + Type string `json:"kind"` + Name string + DisplayName string + Description string + Members []TeamMember + Stacks []TeamStackPermission + Environments []TeamEnvironmentPermission } type TeamMember struct { @@ -52,6 +54,17 @@ type TeamMember struct { Role string } +type TeamStackPermission struct { + ProjectName string `json:"projectName"` + StackName string `json:"stackName"` + Permission int `json:"permission"` +} + +type TeamEnvironmentPermission struct { + EnvName string `json:"envName"` + Permission string `json:"permission"` +} + type createTeamRequest struct { Organization string `json:"organization"` TeamType string `json:"teamType"` @@ -307,3 +320,29 @@ func (c *Client) RemoveStackPermission(ctx context.Context, stack StackName, tea } return nil } + +func (c *Client) GetTeamStackPermission(ctx context.Context, stack StackName, teamName string) (*int, error) { + if len(stack.OrgName) == 0 { + return nil, errors.New("orgname must not be empty") + } + + if len(teamName) == 0 { + return nil, errors.New("teamname must not be empty") + } + + apiPath := path.Join("orgs", stack.OrgName, "teams", teamName) + + var team Team + _, err := c.do(ctx, http.MethodGet, apiPath, nil, &team) + if err != nil { + return nil, fmt.Errorf("failed to get team: %w", err) + } + + for _, stackPermission := range team.Stacks { + if stackPermission.ProjectName == stack.ProjectName && stackPermission.StackName == stack.StackName { + return &stackPermission.Permission, nil + } + } + + return nil, nil +} diff --git a/provider/pkg/provider/team_stack_perm.go b/provider/pkg/provider/team_stack_perm.go index 1816aa64..6de07c65 100644 --- a/provider/pkg/provider/team_stack_perm.go +++ b/provider/pkg/provider/team_stack_perm.go @@ -2,13 +2,15 @@ package provider import ( "context" + "fmt" + "strings" - "github.com/google/uuid" pbempty "google.golang.org/protobuf/types/known/emptypb" "github.com/pulumi/pulumi-pulumiservice/provider/pkg/internal/pulumiapi" "github.com/pulumi/pulumi-pulumiservice/provider/pkg/internal/serde" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" ) @@ -43,12 +45,55 @@ func (tp *TeamStackPermissionResource) Check(req *pulumirpc.CheckRequest) (*pulu }, nil } -func (tp *TeamStackPermissionResource) Configure(config PulumiServiceConfig) { +func (tp *TeamStackPermissionResource) Configure(_ PulumiServiceConfig) { } func (tp *TeamStackPermissionResource) Read(req *pulumirpc.ReadRequest) (*pulumirpc.ReadResponse, error) { - return &pulumirpc.ReadResponse{}, nil + ctx := context.Background() + id := req.GetId() + + permId, err := splitTeamStackPermissionId(id) + if err != nil { + if strings.Contains(err.Error(), "expected 4 parts") { + // Return an error if attempting to refresh stack permissions created before this change. + // We return a warning and an empty response, which will cause the resource to be deleted on refresh, + // forcing the user to recreate it with the updated version. + return nil, fmt.Errorf("TeamStackPermission resources created before v0.17.0 do not support refresh. " + + "You will need to destroy and recreate this resource with >v0.17.0 to successfully refresh.") + } + return nil, err + } + + permission, err := tp.client.GetTeamStackPermission(ctx, pulumiapi.StackName{ + OrgName: permId.Organization, + ProjectName: permId.Project, + StackName: permId.Stack, + }, permId.Team) + if err != nil { + return nil, fmt.Errorf("failed to get team stack permission: %w", err) + } + if permission == nil { + return &pulumirpc.ReadResponse{}, nil + } + + inputs := TeamStackPermissionInput{ + Organization: permId.Organization, + Project: permId.Project, + Stack: permId.Stack, + Team: permId.Team, + Permission: *permission, + } + + properties, err := plugin.MarshalProperties(inputs.ToPropertyMap(), plugin.MarshalOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to marshal inputs to properties: %w", err) + } + return &pulumirpc.ReadResponse{ + Id: req.Id, + Properties: properties, + Inputs: properties, + }, nil } func (tp *TeamStackPermissionResource) Create(req *pulumirpc.CreateRequest) (*pulumirpc.CreateResponse, error) { @@ -69,8 +114,10 @@ func (tp *TeamStackPermissionResource) Create(req *pulumirpc.CreateRequest) (*pu return nil, err } + stackPermissionId := fmt.Sprintf("%s/%s", stackName.String(), inputs.Team) + return &pulumirpc.CreateResponse{ - Id: uuid.NewString(), + Id: stackPermissionId, Properties: req.GetProperties(), }, nil } @@ -109,7 +156,27 @@ func (tp *TeamStackPermissionResource) Diff(req *pulumirpc.DiffRequest) (*pulumi }, nil } -// Update does nothing because we always do a replace on changes, never an update -func (tp *TeamStackPermissionResource) Update(req *pulumirpc.UpdateRequest) (*pulumirpc.UpdateResponse, error) { - return &pulumirpc.UpdateResponse{}, nil +// Update does nothing because we always replace on changes, never an update +func (tp *TeamStackPermissionResource) Update(_ *pulumirpc.UpdateRequest) (*pulumirpc.UpdateResponse, error) { + return nil, fmt.Errorf("unexpected call to update, expected create to be called instead") +} + +type teamStackPermissionId struct { + Organization string + Project string + Stack string + Team string +} + +func splitTeamStackPermissionId(id string) (teamStackPermissionId, error) { + split := strings.Split(id, "/") + if len(split) != 4 { + return teamStackPermissionId{}, fmt.Errorf("invalid id %q, expected 4 parts", id) + } + return teamStackPermissionId{ + Organization: split[0], + Project: split[1], + Stack: split[2], + Team: split[3], + }, nil }