diff --git a/.github/workflows/nightly-tfe-ci.yml b/.github/workflows/nightly-tfe-ci.yml index 85a249771..509cfa4fb 100644 --- a/.github/workflows/nightly-tfe-ci.yml +++ b/.github/workflows/nightly-tfe-ci.yml @@ -20,7 +20,7 @@ jobs: TFE_TOKEN: ${{ secrets.TF_WORKFLOW_TFLOCAL_CLOUD_TFC_TOKEN }} TF_CLOUD_ORGANIZATION: "hashicorp-v2" TF_CLOUD_HOSTNAME: "app.terraform.io" - TF_CLOUD_WORKSPACE: "tflocal-go-tfe-nightly" + TF_WORKSPACE: "tflocal-go-tfe-nightly" run: | cd tflocal/ terraform init diff --git a/CHANGELOG.md b/CHANGELOG.md index ec582f136..aa5747068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# (Unreleased) + +## Enhancements + +* Add `Query` and `Status` fields to `OrganizationMembershipListOptions` to allow filtering memberships by status or username by @sebasslash [#550](https://github.com/hashicorp/go-tfe/pull/550) +* Add `ListForWorkspace` method to `VariableSets` interface to enable fetching variable sets associated with a workspace by @tstapler [#552](https://github.com/hashicorp/go-tfe/pull/552) +* Add `NotificationTriggerAssessmentDrifted` and `NotificationTriggerAssessmentFailed` notification trigger types by @lawliet89 [#542](https://github.com/hashicorp/go-tfe/pull/542) + +## Bug Fixes +* Fix marshalling of run variables in `RunCreateOptions`. The `Variables` field type in `Run` struct has changed from `[]*RunVariable` to `[]*RunVariableAttr` by @Uk1288 [#531](https://github.com/hashicorp/go-tfe/pull/531) + # v1.10.0 ## Enhancements @@ -7,7 +18,6 @@ ## Bug Fixes * Fixes null value returned in variable set relationship in `VariableSetVariable` by @sebasslash [#521](https://github.com/hashicorp/go-tfe/pull/521) -* Fix marshalling of run variables in `RunCreateOptions`. The `Variables` field type in `Run` struct has changed from `[]*RunVariable` to `[]*RunVariableAttr` by @Uk1288 [#531](https://github.com/hashicorp/go-tfe/pull/531) # v1.9.0 diff --git a/README.md b/README.md index f36269c8c..317dd0fde 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,11 @@ This API client covers most of the existing Terraform Cloud API calls and is upd - [x] Policy Checks - [x] Policy Sets - [x] Policy Set Parameters -- [ ] Private Registry +- [x] Private Registry - [x] Modules - [x] Providers - [x] Provider Provider Versions and Platforms - - [ ] GPG Keys + - [x] GPG Keys - [x] Runs - [x] Run Tasks - [ ] Run Tasks Integration diff --git a/helper_test.go b/helper_test.go index 7e8b25f6e..1a3f6b194 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1742,6 +1742,34 @@ func createVariableSet(t *testing.T, client *Client, org *Organization, options } } +func applyVariableSetToWorkspace(t *testing.T, client *Client, vsID string, wsID string) { + if vsID == "" { + t.Fatal("variable set ID must not be empty") + } + + if wsID == "" { + t.Fatal("workspace ID must not be empty") + } + + opts := &VariableSetApplyToWorkspacesOptions{} + opts.Workspaces = append(opts.Workspaces, &Workspace{ID: wsID}) + + ctx := context.Background() + if err := client.VariableSets.ApplyToWorkspaces(ctx, vsID, opts); err != nil { + t.Fatalf("Error applying variable set %s to workspace %s: %v", vsID, wsID, err) + } + + t.Cleanup(func() { + removeOpts := &VariableSetRemoveFromWorkspacesOptions{} + removeOpts.Workspaces = append(removeOpts.Workspaces, &Workspace{ID: wsID}) + if err := client.VariableSets.RemoveFromWorkspaces(ctx, vsID, removeOpts); err != nil { + t.Errorf("Error removing variable set from workspace! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "VariableSet ID: %s\nError: %s", vsID, err) + } + }) +} + func createVariableSetVariable(t *testing.T, client *Client, vs *VariableSet, options VariableSetVariableCreateOptions) (*VariableSetVariable, func()) { var vsCleanup func() diff --git a/mocks/variable_set_mocks.go b/mocks/variable_set_mocks.go index 56d02c9ab..5ac9aa699 100644 --- a/mocks/variable_set_mocks.go +++ b/mocks/variable_set_mocks.go @@ -93,6 +93,21 @@ func (mr *MockVariableSetsMockRecorder) List(ctx, organization, options interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockVariableSets)(nil).List), ctx, organization, options) } +// ListForWorkspace mocks base method. +func (m *MockVariableSets) ListForWorkspace(ctx context.Context, workspaceID string, options *tfe.VariableSetListOptions) (*tfe.VariableSetList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListForWorkspace", ctx, workspaceID, options) + ret0, _ := ret[0].(*tfe.VariableSetList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListForWorkspace indicates an expected call of ListForWorkspace. +func (mr *MockVariableSetsMockRecorder) ListForWorkspace(ctx, workspaceID, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListForWorkspace", reflect.TypeOf((*MockVariableSets)(nil).ListForWorkspace), ctx, workspaceID, options) +} + // Read mocks base method. func (m *MockVariableSets) Read(ctx context.Context, variableSetID string, options *tfe.VariableSetReadOptions) (*tfe.VariableSet, error) { m.ctrl.T.Helper() diff --git a/organization_membership.go b/organization_membership.go index 396b3a94a..db57642a6 100644 --- a/organization_membership.go +++ b/organization_membership.go @@ -80,6 +80,13 @@ type OrganizationMembershipListOptions struct { // Optional: A list of organization member emails to filter by. Emails []string `url:"filter[email],omitempty"` + + // Optional: If specified, restricts results to those matching status value. + Status OrganizationMembershipStatus `url:"filter[status],omitempty"` + + // Optional: A query string to search organization memberships by user name + // and email. + Query string `url:"q,omitempty"` } // OrganizationMembershipCreateOptions represents the options for creating an organization membership. diff --git a/organization_membership_integration_test.go b/organization_membership_integration_test.go index ab4a0381d..76525aae4 100644 --- a/organization_membership_integration_test.go +++ b/organization_membership_integration_test.go @@ -103,6 +103,46 @@ func TestOrganizationMembershipsList(t *testing.T) { }) }) + t.Run("with status filter option", func(t *testing.T) { + _, memTest1Cleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(memTest1Cleanup) + _, memTest2Cleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(memTest2Cleanup) + + ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ + Status: OrganizationMembershipInvited, + }) + require.NoError(t, err) + + require.Len(t, ml.Items, 2) + for _, member := range ml.Items { + assert.Equal(t, member.Status, OrganizationMembershipInvited) + } + }) + + t.Run("with search query string", func(t *testing.T) { + memTest1, memTest1Cleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(memTest1Cleanup) + _, memTest2Cleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(memTest2Cleanup) + _, memTest3Cleanup := createOrganizationMembership(t, client, orgTest) + t.Cleanup(memTest3Cleanup) + + t.Run("using an email", func(t *testing.T) { + ml, err := client.OrganizationMemberships.List(ctx, orgTest.Name, &OrganizationMembershipListOptions{ + Query: memTest1.Email, + }) + require.NoError(t, err) + + require.Len(t, ml.Items, 1) + assert.Equal(t, ml.Items[0].Email, memTest1.Email) + }) + + t.Run("using a user name", func(t *testing.T) { + t.Skip("Skipping, missing Account API support in order to set usernames") + }) + }) + t.Run("without a valid organization", func(t *testing.T) { ml, err := client.OrganizationMemberships.List(ctx, badIdentifier, nil) assert.Nil(t, ml) diff --git a/variable_set.go b/variable_set.go index 3a9a1f9ec..6bc37c6a5 100644 --- a/variable_set.go +++ b/variable_set.go @@ -17,6 +17,9 @@ type VariableSets interface { // List all the variable sets within an organization. List(ctx context.Context, organization string, options *VariableSetListOptions) (*VariableSetList, error) + // ListForWorkspace gets the associated variable sets for a workspace. + ListForWorkspace(ctx context.Context, workspaceID string, options *VariableSetListOptions) (*VariableSetList, error) + // Create is used to create a new variable set. Create(ctx context.Context, organization string, options *VariableSetCreateOptions) (*VariableSet, error) @@ -179,6 +182,32 @@ func (s *variableSets) List(ctx context.Context, organization string, options *V return vl, nil } +// ListForWorkspace gets the associated variable sets for a workspace. +func (s *variableSets) ListForWorkspace(ctx context.Context, workspaceID string, options *VariableSetListOptions) (*VariableSetList, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + if options != nil { + if err := options.valid(); err != nil { + return nil, err + } + } + + u := fmt.Sprintf("workspaces/%s/varsets", url.QueryEscape(workspaceID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + vl := &VariableSetList{} + err = req.Do(ctx, vl) + if err != nil { + return nil, err + } + + return vl, nil +} + // Create is used to create a new variable set. func (s *variableSets) Create(ctx context.Context, organization string, options *VariableSetCreateOptions) (*VariableSet, error) { if !validStringID(&organization) { diff --git a/variable_set_test.go b/variable_set_test.go index 8af5b7652..c3fcd7d98 100644 --- a/variable_set_test.go +++ b/variable_set_test.go @@ -51,13 +51,67 @@ func TestVariableSetsList(t *testing.T) { assert.Equal(t, 2, vsl.TotalCount) }) - t.Run("when Organization name is invalid ID", func(t *testing.T) { + t.Run("when Organization name is an invalid ID", func(t *testing.T) { vsl, err := client.VariableSets.List(ctx, badIdentifier, nil) assert.Nil(t, vsl) assert.EqualError(t, err, ErrInvalidOrg.Error()) }) } +func TestVariableSetsListForWorkspace(t *testing.T) { + skipIfNotCINode(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + t.Cleanup(orgTestCleanup) + workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest) + t.Cleanup(workspaceTestCleanup) + + vsTest1, vsTestCleanup1 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{}) + t.Cleanup(vsTestCleanup1) + vsTest2, vsTestCleanup2 := createVariableSet(t, client, orgTest, VariableSetCreateOptions{}) + t.Cleanup(vsTestCleanup2) + + applyVariableSetToWorkspace(t, client, vsTest1.ID, workspaceTest.ID) + applyVariableSetToWorkspace(t, client, vsTest2.ID, workspaceTest.ID) + + t.Run("without list options", func(t *testing.T) { + vsl, err := client.VariableSets.ListForWorkspace(ctx, workspaceTest.ID, nil) + require.NoError(t, err) + require.Len(t, vsl.Items, 2) + + ids := []string{vsTest1.ID, vsTest2.ID} + for _, varset := range vsl.Items { + assert.Contains(t, ids, varset.ID) + } + }) + + t.Run("with list options", func(t *testing.T) { + t.Skip("paging not supported yet in API") + // Request a page number which is out of range. The result should + // be successful, but return no results if the paging options are + // properly passed along. + vsl, err := client.VariableSets.ListForWorkspace(ctx, workspaceTest.ID, &VariableSetListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }) + require.NoError(t, err) + assert.Empty(t, vsl.Items) + assert.Equal(t, 999, vsl.CurrentPage) + assert.Equal(t, 2, vsl.TotalCount) + }) + + t.Run("when Workspace ID is an invalid ID", func(t *testing.T) { + vsl, err := client.VariableSets.ListForWorkspace(ctx, badIdentifier, nil) + assert.Nil(t, vsl) + assert.EqualError(t, err, ErrInvalidWorkspaceID.Error()) + }) +} + func TestVariableSetsCreate(t *testing.T) { skipIfNotCINode(t)