diff --git a/CHANGELOG.md b/CHANGELOG.md index ca91d022e..9980fffd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ * Adds support for a new variable field `version-id` by @arybolovlev [#697](https://github.com/hashicorp/go-tfe/pull/697) * Adds `ExpiredAt` field to `OrganizationToken`, `TeamToken`, and `UserToken`. This feature will be available in TFE release, v202305-1. @JuliannaTetreault [#672](https://github.com/hashicorp/go-tfe/pull/672) +## Bug Fixes +* AgentPool `Update` was previously not able to remove all allowed workspaces from an agent pool. `AllowedWorkspaces` has been removed from `AgentPoolUpdateOptions` and are now handled by a separate `UpdateAllowedWorkspaces` method using `AgentPoolAllowedWorkspacesUpdateOptions`. + + # v1.23.0 ## Features diff --git a/agent_pool.go b/agent_pool.go index 43f564c15..0b47e2dbf 100644 --- a/agent_pool.go +++ b/agent_pool.go @@ -32,6 +32,9 @@ type AgentPools interface { // Update an agent pool by its ID. Update(ctx context.Context, agentPool string, options AgentPoolUpdateOptions) (*AgentPool, error) + // UpdateAllowedWorkspaces updates the list of allowed workspaces associated with an agent pool. + UpdateAllowedWorkspaces(ctx context.Context, agentPool string, options AgentPoolUpdateAllowedWorkspacesOptions) (*AgentPool, error) + // Delete an agent pool by its ID. Delete(ctx context.Context, agentPoolID string) error } @@ -189,13 +192,22 @@ type AgentPoolUpdateOptions struct { Type string `jsonapi:"primary,agent-pools"` // A new name to identify the agent pool. - Name *string `jsonapi:"attr,name"` + Name *string `jsonapi:"attr,name,omitempty"` // True if the agent pool is organization scoped, false otherwise. OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"` +} + +// AgentPoolUpdateAllowedWorkspacesOptions represents the options for updating allowed workspace on an agent pool +type AgentPoolUpdateAllowedWorkspacesOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,agent-pools"` // A new list of workspaces that are associated with an agent pool. - AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces,omitempty"` + AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"` } // Update an agent pool by its ID. @@ -223,6 +235,26 @@ func (s *agentPools) Update(ctx context.Context, agentPoolID string, options Age return k, nil } +func (s *agentPools) UpdateAllowedWorkspaces(ctx context.Context, agentPoolID string, options AgentPoolUpdateAllowedWorkspacesOptions) (*AgentPool, error) { + if !validStringID(&agentPoolID) { + return nil, ErrInvalidAgentPoolID + } + + u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID)) + req, err := s.client.NewRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + k := &AgentPool{} + err = req.Do(ctx, k) + if err != nil { + return nil, err + } + + return k, nil +} + // Delete an agent pool by its ID. func (s *agentPools) Delete(ctx context.Context, agentPoolID string) error { if !validStringID(&agentPoolID) { diff --git a/agent_pool_integration_test.go b/agent_pool_integration_test.go index 7fecd6a5a..2b9fe0388 100644 --- a/agent_pool_integration_test.go +++ b/agent_pool_integration_test.go @@ -270,9 +270,20 @@ func TestAgentPoolsUpdate(t *testing.T) { assert.NotEqual(t, kBefore.Name, kAfter.Name) }) - t.Run("when updating the name", func(t *testing.T) { - kBefore, kTestCleanup := createAgentPool(t, client, orgTest) - defer kTestCleanup() + t.Run("when updating only the name", func(t *testing.T) { + workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest) + defer workspaceTestCleanup() + + organizationScoped := false + options := AgentPoolCreateOptions{ + Name: String("a-pool"), + OrganizationScoped: &organizationScoped, + AllowedWorkspaces: []*Workspace{ + workspaceTest, + }, + } + kBefore, err := client.AgentPools.Create(ctx, orgTest.Name, options) + require.NoError(t, err) kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{ Name: String("updated-key-name"), @@ -281,6 +292,8 @@ func TestAgentPoolsUpdate(t *testing.T) { assert.Equal(t, kBefore.ID, kAfter.ID) assert.Equal(t, "updated-key-name", kAfter.Name) + assert.Equal(t, 1, len(kAfter.AllowedWorkspaces)) + assert.Equal(t, workspaceTest.ID, kAfter.AllowedWorkspaces[0].ID) }) t.Run("without a valid agent pool ID", func(t *testing.T) { @@ -289,6 +302,31 @@ func TestAgentPoolsUpdate(t *testing.T) { assert.EqualError(t, err, ErrInvalidAgentPoolID.Error()) }) + t.Run("when updating organization scope", func(t *testing.T) { + kBefore, kTestCleanup := createAgentPool(t, client, orgTest) + defer kTestCleanup() + + organizationScoped := false + kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{ + Name: String(kBefore.Name), + OrganizationScoped: &organizationScoped, + }) + require.NoError(t, err) + + assert.NotEqual(t, kBefore.OrganizationScoped, kAfter.OrganizationScoped) + assert.Equal(t, organizationScoped, kAfter.OrganizationScoped) + }) +} + +func TestAgentPoolsUpdateAllowedWorkspaces(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + upgradeOrganizationSubscription(t, client, orgTest) + t.Run("when updating allowed-workspaces", func(t *testing.T) { kBefore, kTestCleanup := createAgentPool(t, client, orgTest) defer kTestCleanup() @@ -296,32 +334,43 @@ func TestAgentPoolsUpdate(t *testing.T) { workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest) defer workspaceTestCleanup() - kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{ - Name: String(kBefore.Name), + kAfter, err := client.AgentPools.UpdateAllowedWorkspaces(ctx, kBefore.ID, AgentPoolUpdateAllowedWorkspacesOptions{ AllowedWorkspaces: []*Workspace{ workspaceTest, }, }) require.NoError(t, err) + assert.Equal(t, kBefore.Name, kAfter.Name) assert.NotEqual(t, kBefore.AllowedWorkspaces, kAfter.AllowedWorkspaces) assert.Equal(t, 1, len(kAfter.AllowedWorkspaces)) assert.Equal(t, workspaceTest.ID, kAfter.AllowedWorkspaces[0].ID) }) - t.Run("when updating organization scope", func(t *testing.T) { - kBefore, kTestCleanup := createAgentPool(t, client, orgTest) - defer kTestCleanup() + t.Run("when removing all the allowed-workspaces", func(t *testing.T) { + workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest) + defer workspaceTestCleanup() organizationScoped := false - kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{ - Name: String(kBefore.Name), + options := AgentPoolCreateOptions{ + Name: String("a-pool"), OrganizationScoped: &organizationScoped, + AllowedWorkspaces: []*Workspace{ + workspaceTest, + }, + } + + kBefore, kTestCleanup := createAgentPoolWithOptions(t, client, orgTest, options) + defer kTestCleanup() + + kAfter, err := client.AgentPools.UpdateAllowedWorkspaces(ctx, kBefore.ID, AgentPoolUpdateAllowedWorkspacesOptions{ + AllowedWorkspaces: []*Workspace{}, }) require.NoError(t, err) - assert.NotEqual(t, kBefore.OrganizationScoped, kAfter.OrganizationScoped) - assert.Equal(t, organizationScoped, kAfter.OrganizationScoped) + assert.Equal(t, kBefore.ID, kAfter.ID) + assert.Equal(t, "a-pool", kAfter.Name) + assert.Empty(t, kAfter.AllowedWorkspaces) }) } diff --git a/mocks/agent_pool_mocks.go b/mocks/agent_pool_mocks.go index 8295693b6..96276e3ea 100644 --- a/mocks/agent_pool_mocks.go +++ b/mocks/agent_pool_mocks.go @@ -123,3 +123,18 @@ func (mr *MockAgentPoolsMockRecorder) Update(ctx, agentPool, options interface{} mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAgentPools)(nil).Update), ctx, agentPool, options) } + +// UpdateAllowedWorkspaces mocks base method. +func (m *MockAgentPools) UpdateAllowedWorkspaces(ctx context.Context, agentPool string, options tfe.AgentPoolUpdateAllowedWorkspacesOptions) (*tfe.AgentPool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAllowedWorkspaces", ctx, agentPool, options) + ret0, _ := ret[0].(*tfe.AgentPool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAllowedWorkspaces indicates an expected call of UpdateAllowedWorkspaces. +func (mr *MockAgentPoolsMockRecorder) UpdateAllowedWorkspaces(ctx, agentPool, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAllowedWorkspaces", reflect.TypeOf((*MockAgentPools)(nil).UpdateAllowedWorkspaces), ctx, agentPool, options) +} diff --git a/mocks/registry_no_code_module_mocks.go b/mocks/registry_no_code_module_mocks.go new file mode 100644 index 000000000..b3b28f4bd --- /dev/null +++ b/mocks/registry_no_code_module_mocks.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: registry_no_code_module.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + tfe "github.com/hashicorp/go-tfe" +) + +// MockRegistryNoCodeModules is a mock of RegistryNoCodeModules interface. +type MockRegistryNoCodeModules struct { + ctrl *gomock.Controller + recorder *MockRegistryNoCodeModulesMockRecorder +} + +// MockRegistryNoCodeModulesMockRecorder is the mock recorder for MockRegistryNoCodeModules. +type MockRegistryNoCodeModulesMockRecorder struct { + mock *MockRegistryNoCodeModules +} + +// NewMockRegistryNoCodeModules creates a new mock instance. +func NewMockRegistryNoCodeModules(ctrl *gomock.Controller) *MockRegistryNoCodeModules { + mock := &MockRegistryNoCodeModules{ctrl: ctrl} + mock.recorder = &MockRegistryNoCodeModulesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRegistryNoCodeModules) EXPECT() *MockRegistryNoCodeModulesMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockRegistryNoCodeModules) Create(ctx context.Context, organization string, options tfe.RegistryNoCodeModuleCreateOptions) (*tfe.RegistryNoCodeModule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, organization, options) + ret0, _ := ret[0].(*tfe.RegistryNoCodeModule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockRegistryNoCodeModulesMockRecorder) Create(ctx, organization, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Create), ctx, organization, options) +} + +// Delete mocks base method. +func (m *MockRegistryNoCodeModules) Delete(ctx context.Context, ID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, ID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRegistryNoCodeModulesMockRecorder) Delete(ctx, ID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Delete), ctx, ID) +} + +// Read mocks base method. +func (m *MockRegistryNoCodeModules) Read(ctx context.Context, noCodeModuleID string, options *tfe.RegistryNoCodeModuleReadOptions) (*tfe.RegistryNoCodeModule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, noCodeModuleID, options) + ret0, _ := ret[0].(*tfe.RegistryNoCodeModule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockRegistryNoCodeModulesMockRecorder) Read(ctx, noCodeModuleID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Read), ctx, noCodeModuleID, options) +} + +// Update mocks base method. +func (m *MockRegistryNoCodeModules) Update(ctx context.Context, noCodeModuleID string, options tfe.RegistryNoCodeModuleUpdateOptions) (*tfe.RegistryNoCodeModule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, noCodeModuleID, options) + ret0, _ := ret[0].(*tfe.RegistryNoCodeModule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRegistryNoCodeModulesMockRecorder) Update(ctx, noCodeModuleID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRegistryNoCodeModules)(nil).Update), ctx, noCodeModuleID, options) +}