diff --git a/errors.go b/errors.go index a3f7dabc6..27fb84347 100644 --- a/errors.go +++ b/errors.go @@ -310,6 +310,8 @@ var ( ErrRequiredProjectID = errors.New("project ID is required") + ErrRequiredStackID = errors.New("stack ID is required") + ErrWorkspacesRequired = errors.New("workspaces is required") ErrWorkspaceMinLimit = errors.New("must provide at least one workspace") diff --git a/variable_set.go b/variable_set.go index 5a100f0bd..ca9806984 100644 --- a/variable_set.go +++ b/variable_set.go @@ -50,8 +50,17 @@ type VariableSets interface { // Remove variable set from projects in the supplied list. RemoveFromProjects(ctx context.Context, variableSetID string, options VariableSetRemoveFromProjectsOptions) error + // Apply variable set to stacks in the supplied list. + ApplyToStacks(ctx context.Context, variableSetID string, options *VariableSetApplyToStacksOptions) error + + // Remove variable set from stacks in the supplied list. + RemoveFromStacks(ctx context.Context, variableSetID string, options *VariableSetRemoveFromStacksOptions) error + // Update list of workspaces to which the variable set is applied to match the supplied list. UpdateWorkspaces(ctx context.Context, variableSetID string, options *VariableSetUpdateWorkspacesOptions) (*VariableSet, error) + + // Update list of stacks to which the variable set is applied to match the supplied list. + // UpdateStacks(ctx context.Context, variableSetID string, options *VariableSetUpdateStacksOptions) (*VariableSet, error) } // variableSets implements VariableSets. @@ -87,6 +96,7 @@ type VariableSet struct { Parent *Parent `jsonapi:"polyrelation,parent"` Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"` Projects []*Project `jsonapi:"relation,projects,omitempty"` + Stacks []*Stack `jsonapi:"relation,stacks,omitempty"` Variables []*VariableSetVariable `jsonapi:"relation,vars,omitempty"` } @@ -98,6 +108,7 @@ const ( VariableSetWorkspaces VariableSetIncludeOpt = "workspaces" VariableSetProjects VariableSetIncludeOpt = "projects" VariableSetVars VariableSetIncludeOpt = "vars" + VariableSetStacks VariableSetIncludeOpt = "stacks" ) // VariableSetListOptions represents the options for listing variable sets. @@ -191,6 +202,18 @@ type VariableSetRemoveFromProjectsOptions struct { Projects []*Project } +// VariableSetApplyToStacksOptions represents the options for applying variable sets to stacks. +type VariableSetApplyToStacksOptions struct { + // The stacks to apply the variable set to (additive). + Stacks []*Stack +} + +// VariableSetRemoveFromStacksOptions represents the options for removing variable sets from stacks. +type VariableSetRemoveFromStacksOptions struct { + // The stacks to remove the variable set from. + Stacks []*Stack +} + // VariableSetUpdateWorkspacesOptions represents a subset of update options specifically for applying variable sets to workspaces type VariableSetUpdateWorkspacesOptions struct { // Type is a public field utilized by JSON:API to @@ -444,6 +467,45 @@ func (s variableSets) RemoveFromProjects(ctx context.Context, variableSetID stri return req.Do(ctx, nil) } +// ApplyToStacks applies the variable set to stacks in the supplied list. +// This method will return an error if the variable set has global = true. +func (s *variableSets) ApplyToStacks(ctx context.Context, variableSetID string, options *VariableSetApplyToStacksOptions) error { + if !validStringID(&variableSetID) { + return ErrInvalidVariableSetID + } + if err := options.valid(); err != nil { + return err + } + + u := fmt.Sprintf("varsets/%s/relationships/stacks", url.PathEscape(variableSetID)) + req, err := s.client.NewRequest("POST", u, options.Stacks) + if err != nil { + return err + } + a := req.Do(ctx, nil) + fmt.Printf("a: %v\n", a) + return a +} + +// RemoveFromStacks removes the variable set from stacks in the supplied list. +// This method will return an error if the variable set has global = true. +func (s *variableSets) RemoveFromStacks(ctx context.Context, variableSetID string, options *VariableSetRemoveFromStacksOptions) error { + if !validStringID(&variableSetID) { + return ErrInvalidVariableSetID + } + if err := options.valid(); err != nil { + return err + } + + u := fmt.Sprintf("varsets/%s/relationships/stacks", url.PathEscape(variableSetID)) + req, err := s.client.NewRequest("DELETE", u, options.Stacks) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + // Update variable set to be applied to only the workspaces in the supplied list. func (s *variableSets) UpdateWorkspaces(ctx context.Context, variableSetID string, options *VariableSetUpdateWorkspacesOptions) (*VariableSet, error) { if err := options.valid(); err != nil { @@ -525,6 +587,24 @@ func (o VariableSetRemoveFromProjectsOptions) valid() error { return nil } +func (o VariableSetApplyToStacksOptions) valid() error { + for _, s := range o.Stacks { + if !validStringID(&s.ID) { + return ErrRequiredStackID + } + } + return nil +} + +func (o VariableSetRemoveFromStacksOptions) valid() error { + for _, s := range o.Stacks { + if !validStringID(&s.ID) { + return ErrRequiredStackID + } + } + return nil +} + func (o *VariableSetUpdateWorkspacesOptions) valid() error { if o == nil || o.Workspaces == nil { return ErrRequiredWorkspacesList diff --git a/variable_set_test.go b/variable_set_test.go index 4c923c1f8..d5e466234 100644 --- a/variable_set_test.go +++ b/variable_set_test.go @@ -5,6 +5,7 @@ package tfe import ( "context" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -564,6 +565,157 @@ func TestVariableSetsApplyToAndRemoveFromProjects(t *testing.T) { }) } +func TestVariableSetsApplyToAndRemoveFromStacks(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + t.Log("Creating organization...") + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + t.Logf("Created org: %s", orgTest.Name) + + t.Log("Creating variable set...") + vsTest, vsTestCleanup := createVariableSet(t, client, orgTest, VariableSetCreateOptions{}) + t.Cleanup(vsTestCleanup) + t.Logf("Created variable set: %s", vsTest.ID) + + t.Log("Creating OAuth client...") + oauthClient, cleanup := createOAuthClient(t, client, orgTest, nil) + t.Cleanup(cleanup) + t.Log("Created OAuth client") + + t.Log("Creating first stack...") + stackTest1, err := client.Stacks.Create(ctx, StackCreateOptions{ + Name: "test-stack-1", + VCSRepo: &StackVCSRepoOptions{ + Identifier: "nithishravindra/pet-nulls-stack", + OAuthTokenID: oauthClient.OAuthTokens[0].ID, + }, + Project: &Project{ + ID: orgTest.DefaultProject.ID, + }, + }) + require.NoError(t, err) + t.Logf("Created stack 1: %s", stackTest1.ID) + t.Cleanup(func() { + if err := client.Stacks.Delete(ctx, stackTest1.ID); err != nil { + t.Logf("Failed to cleanup stack %s: %v", stackTest1.ID, err) + } + }) + + // Wait for stack to be ready by triggering configuration update + _, err = client.Stacks.UpdateConfiguration(ctx, stackTest1.ID) + // Don't require this to succeed as it might not be needed + + stackTest2, err := client.Stacks.Create(ctx, StackCreateOptions{ + Name: "test-stack-2", + VCSRepo: &StackVCSRepoOptions{ + Identifier: "nithishravindra/pet-nulls-stack", + OAuthTokenID: oauthClient.OAuthTokens[0].ID, + }, + Project: &Project{ + ID: orgTest.DefaultProject.ID, + }, + }) + require.NoError(t, err) + t.Cleanup(func() { + if err := client.Stacks.Delete(ctx, stackTest2.ID); err != nil { + t.Logf("Failed to cleanup stack %s: %v", stackTest2.ID, err) + } + }) + + // Wait for stack to be ready by triggering configuration update + _, err = client.Stacks.UpdateConfiguration(ctx, stackTest2.ID) + // Don't require this to succeed as it might not be needed + + t.Run("with first stack added", func(t *testing.T) { + options := VariableSetApplyToStacksOptions{ + Stacks: []*Stack{{ID: stackTest1.ID}}, // Use minimal stack object with just ID + } + fmt.Printf("ctx: %v\n", ctx) + fmt.Printf("vsTest.ID: %v\n", vsTest.ID) + fmt.Printf("vsTest: %v\n", vsTest) + fmt.Printf("options: %v\n", options) + err = client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &options) + fmt.Printf("err: %v\n", err) + require.NoError(t, err) + + readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}} + vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts) + require.NoError(t, err) + + assert.Equal(t, 1, len(vsAfter.Stacks)) + assert.Equal(t, stackTest1.ID, vsAfter.Stacks[0].ID) + }) + + t.Run("with second stack added", func(t *testing.T) { + options := VariableSetApplyToStacksOptions{ + Stacks: []*Stack{stackTest2}, + } + + err := client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &options) + require.NoError(t, err) + + readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}} + vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts) + require.NoError(t, err) + + assert.Equal(t, 2, len(vsAfter.Stacks)) + stackIDs := []string{vsAfter.Stacks[0].ID, vsAfter.Stacks[1].ID} + assert.Contains(t, stackIDs, stackTest1.ID) + assert.Contains(t, stackIDs, stackTest2.ID) + }) + + t.Run("with first stack removed", func(t *testing.T) { + options := VariableSetRemoveFromStacksOptions{ + Stacks: []*Stack{stackTest1}, + } + + err := client.VariableSets.RemoveFromStacks(ctx, vsTest.ID, &options) + require.NoError(t, err) + + readOpts := &VariableSetReadOptions{Include: &[]VariableSetIncludeOpt{VariableSetStacks}} + vsAfter, err := client.VariableSets.Read(ctx, vsTest.ID, readOpts) + require.NoError(t, err) + + assert.Equal(t, 1, len(vsAfter.Stacks)) + assert.Equal(t, stackTest2.ID, vsAfter.Stacks[0].ID) + }) + + t.Run("when variable set ID is invalid", func(t *testing.T) { + applyOptions := VariableSetApplyToStacksOptions{ + Stacks: []*Stack{stackTest1}, + } + err := client.VariableSets.ApplyToStacks(ctx, badIdentifier, &applyOptions) + assert.EqualError(t, err, ErrInvalidVariableSetID.Error()) + + removeOptions := VariableSetRemoveFromStacksOptions{ + Stacks: []*Stack{stackTest1}, + } + err = client.VariableSets.RemoveFromStacks(ctx, badIdentifier, &removeOptions) + assert.EqualError(t, err, ErrInvalidVariableSetID.Error()) + }) + + t.Run("when stack ID is invalid", func(t *testing.T) { + badStack := &Stack{ + ID: badIdentifier, + } + + applyOptions := VariableSetApplyToStacksOptions{ + Stacks: []*Stack{badStack}, + } + err := client.VariableSets.ApplyToStacks(ctx, vsTest.ID, &applyOptions) + assert.EqualError(t, err, ErrRequiredStackID.Error()) + + removeOptions := VariableSetRemoveFromStacksOptions{ + Stacks: []*Stack{badStack}, + } + err = client.VariableSets.RemoveFromStacks(ctx, vsTest.ID, &removeOptions) + assert.EqualError(t, err, ErrRequiredStackID.Error()) + }) + +} + func TestVariableSetsUpdateWorkspaces(t *testing.T) { client := testClient(t) ctx := context.Background()