From 2c1cb2bbb18a3bd8be3b0419ffb4e1102b1b91f1 Mon Sep 17 00:00:00 2001 From: Lauren Date: Mon, 25 Jul 2022 15:43:17 -0400 Subject: [PATCH] adding new data source --- .circleci/config.yml | 6 +- .github/CODEOWNERS | 3 - CHANGELOG.md | 28 +- CODEOWNERS | 3 +- README.md | 1 + admin_organization.go | 26 +- admin_organization_integration_test.go | 53 +- admin_run.go | 8 +- admin_run_integration_test.go | 47 +- admin_setting_cost_estimation.go | 8 +- admin_setting_customization.go | 8 +- admin_setting_general.go | 8 +- admin_setting_saml.go | 12 +- admin_setting_smtp.go | 8 +- admin_setting_twilio.go | 12 +- admin_terraform_version.go | 20 +- admin_user.go | 28 +- admin_user_integration_test.go | 14 +- admin_workspace.go | 12 +- admin_workspace_integration_test.go | 24 +- agent.go | 173 +++++++ agent_integration_test.go | 114 +++++ agent_pool.go | 43 +- agent_pool_integration_test.go | 54 ++- agent_token.go | 16 +- agent_token_integration_test.go | 4 +- apply.go | 4 +- apply_integration_test.go | 4 +- audit_trail_integration_test.go | 10 +- comment.go | 12 +- configuration_version.go | 27 +- configuration_version_integration_test.go | 90 ++-- cost_estimate.go | 8 +- docs/CONTRIBUTING.md | 31 +- docs/TESTS.md | 2 +- errors.go | 14 +- generate_mocks.sh | 1 + gpg_key.go | 16 +- helper_test.go | 453 ++++++++++++++---- ip_ranges.go | 4 +- notification_configuration.go | 24 +- oauth_client.go | 21 +- oauth_client_integration_test.go | 25 +- oauth_token.go | 16 +- organization.go | 32 +- organization_membership.go | 16 +- organization_tags.go | 12 +- organization_token.go | 12 +- organization_token_integration_test.go | 4 +- plan.go | 8 +- plan_export.go | 16 +- plan_export_integration_test.go | 6 +- policy.go | 28 +- policy_check.go | 16 +- policy_check_integration_test.go | 3 + policy_integration_test.go | 8 +- policy_set.go | 36 +- policy_set_integration_test.go | 2 + policy_set_parameter.go | 20 +- policy_set_parameter_integration_test.go | 2 +- policy_set_version.go | 12 +- registry_module.go | 94 +++- registry_module_integration_test.go | 213 ++++++-- registry_provider.go | 16 +- registry_provider_integration_test.go | 13 +- registry_provider_platform.go | 22 +- ...stry_provider_platform_integration_test.go | 5 +- registry_provider_version.go | 20 +- registry_provider_version_integration_test.go | 15 +- request.go | 104 ++++ run.go | 28 +- run_integration_test.go | 34 +- run_task.go | 48 +- run_task_integration_test.go | 57 ++- run_trigger.go | 16 +- run_trigger_integration_test.go | 3 +- ssh_key.go | 20 +- state_version.go | 32 +- state_version_integration_test.go | 15 +- state_version_output.go | 8 +- state_version_output_integration_test.go | 14 +- task_result.go | 4 +- task_stages.go | 8 +- task_stages_integration_test.go | 6 +- team.go | 20 +- team_access.go | 20 +- team_member.go | 26 +- team_member_integration_test.go | 4 +- team_token.go | 12 +- team_token_integration_test.go | 4 +- test-fixtures/ext-state-version/state.json | 163 ------- .../json-state-outputs/everything.json | 113 +++++ testing.go | 4 +- tfe.go | 243 +++------- tfe_integration_test.go | 8 +- tfe_test.go | 4 +- user.go | 8 +- user_integration_test.go | 9 +- user_token.go | 16 +- variable.go | 20 +- variable_integration_test.go | 2 +- variable_set.go | 32 +- variable_set_test.go | 1 + variable_set_variable.go | 20 +- variable_set_variable_test.go | 5 +- workspace.go | 119 +++-- workspace_integration_test.go | 196 +++++++- workspace_run_task.go | 20 +- workspace_run_task_integration_test.go | 17 +- 109 files changed, 2367 insertions(+), 1242 deletions(-) delete mode 100644 .github/CODEOWNERS create mode 100644 agent.go create mode 100644 agent_integration_test.go create mode 100644 request.go delete mode 100644 test-fixtures/ext-state-version/state.json create mode 100644 test-fixtures/json-state-outputs/everything.json diff --git a/.circleci/config.yml b/.circleci/config.yml index fac67a1c4..d4804c2e8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,6 +15,10 @@ jobs: steps: - checkout + - setup_remote_docker: + version: 20.10.14 + docker_layer_caching: true + - run: name: Make test results directory command: mkdir -p $TEST_RESULTS_DIR @@ -36,4 +40,4 @@ workflows: my-workflow: jobs: - run-tests: - context: core-team-access + context: core-team-access \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 653fd5654..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ - -# This whitelists the whole repo -* @hashicorp/tf-practitioner diff --git a/CHANGELOG.md b/CHANGELOG.md index f19c8d10a..e17c0c9dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,33 @@ -# v1.4.0 (Unreleased) +# Unreleased + +# v1.6.0 +## Enhancements +* Add `Name` field to `OAuthClient` by @barrettclark [#466](https://github.com/hashicorp/go-tfe/pull/466) +* Add support for creating both public and private `RegistryModule` with no VCS connection by @Uk1288 [#460](https://github.com/hashicorp/go-tfe/pull/460) +* Add `ConfigurationSourceAdo` configuration source option by @mjyocca [#467](https://github.com/hashicorp/go-tfe/pull/467) + +# v1.5.0 + +## Enhancements +* [beta] Add support for triggering Workspace runs through matching Git tags [#434](https://github.com/hashicorp/go-tfe/pull/434) +* Add `Query` param field to `AgentPoolListOptions` to allow searching based on agent pool name, by @JarrettSpiker [#417](https://github.com/hashicorp/go-tfe/pull/417) +* Add organization scope and allowed workspaces field for scope agents by @Netra2104 [#453](https://github.com/hashicorp/go-tfe/pull/453) +* Adds `Namespace` and `RegistryName` fields to `RegistryModuleID` to allow reading of Public Registry Modules by @Uk1288 [#464](https://github.com/hashicorp/go-tfe/pull/464) + +## Bug fixes +* Fixed JSON mapping for Configuration Versions failing to properly set the `speculative` property [#459](https://github.com/hashicorp/go-tfe/pull/459) + +# v1.4.0 ## Enhancements * Adds `RetryServerErrors` field to the `Config` object by @sebasslash [#439](https://github.com/hashicorp/go-tfe/pull/439) * Adds support for the GPG Keys API by @sebasslash [#429](https://github.com/hashicorp/go-tfe/pull/429) -* [beta] Renames the optional StateVersion field `ExtState` to `JSONState` and changes to string for base64 encoding by @annawinkler [#444](https://github.com/hashicorp/go-tfe/pull/444) +* Adds support for new `WorkspaceLimit` Admin setting for organizations [#425](https://github.com/hashicorp/go-tfe/pull/425) +* Adds support for new `ExcludeTags` workspace list filter field by @Uk1288 [#438](https://github.com/hashicorp/go-tfe/pull/438) +* [beta] Adds additional filter fields to `RunListOptions` by @mjyocca [#424](https://github.com/hashicorp/go-tfe/pull/424) +* [beta] Renames the optional StateVersion field `ExtState` to `JSONStateOutputs` and changes the purpose and type by @annawinkler [#444](https://github.com/hashicorp/go-tfe/pull/444) and @brandoncroft [#452](https://github.com/hashicorp/go-tfe/pull/452) +* Remove beta messaging for Run Tasks by @glennsarti [#447](https://github.com/hashicorp/go-tfe/pull/447) +* Adds `Description` field to the `RunTask` object by @glennsarti [#447](https://github.com/hashicorp/go-tfe/pull/447) # v1.3.0 diff --git a/CODEOWNERS b/CODEOWNERS index 989a818a8..0ff5bf358 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1 @@ -*.go @hashicorp/tf-cli -*.md @hashicorp/tf-cli +* @hashicorp/tf-cli diff --git a/README.md b/README.md index 60c6df8bc..60b7fb569 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ For complete usage of the API client, see the [full package docs](https://pkg.go This API client covers most of the existing Terraform Cloud API calls and is updated regularly to add new or missing endpoints. - [x] Account +- [x] Agents - [x] Agent Pools - [x] Agent Tokens - [x] Applies diff --git a/admin_organization.go b/admin_organization.go index c43013743..abb03da88 100644 --- a/admin_organization.go +++ b/admin_organization.go @@ -50,6 +50,7 @@ type AdminOrganization struct { TerraformBuildWorkerApplyTimeout string `jsonapi:"attr,terraform-build-worker-apply-timeout"` TerraformBuildWorkerPlanTimeout string `jsonapi:"attr,terraform-build-worker-plan-timeout"` TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled"` + WorkspaceLimit *int `jsonapi:"attr,workspace-limit"` // Relations Owners []*User `jsonapi:"relation,owners"` @@ -64,6 +65,7 @@ type AdminOrganizationUpdateOptions struct { TerraformBuildWorkerApplyTimeout *string `jsonapi:"attr,terraform-build-worker-apply-timeout,omitempty"` TerraformBuildWorkerPlanTimeout *string `jsonapi:"attr,terraform-build-worker-plan-timeout,omitempty"` TerraformWorkerSudoEnabled bool `jsonapi:"attr,terraform-worker-sudo-enabled,omitempty"` + WorkspaceLimit *int `jsonapi:"attr,workspace-limit,omitempty"` } // AdminOrganizationList represents a list of organizations via Admin API. @@ -105,13 +107,13 @@ func (s *adminOrganizations) List(ctx context.Context, options *AdminOrganizatio return nil, err } u := "admin/organizations" - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } orgl := &AdminOrganizationList{} - err = s.client.do(ctx, req, orgl) + err = req.Do(ctx, orgl) if err != nil { return nil, err } @@ -127,13 +129,13 @@ func (s *adminOrganizations) ListModuleConsumers(ctx context.Context, organizati u := fmt.Sprintf("admin/organizations/%s/relationships/module-consumers", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } orgl := &AdminOrganizationList{} - err = s.client.do(ctx, req, orgl) + err = req.Do(ctx, orgl) if err != nil { return nil, err } @@ -148,13 +150,13 @@ func (s *adminOrganizations) Read(ctx context.Context, organization string) (*Ad } u := fmt.Sprintf("admin/organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } org := &AdminOrganization{} - err = s.client.do(ctx, req, org) + err = req.Do(ctx, org) if err != nil { return nil, err } @@ -169,13 +171,13 @@ func (s *adminOrganizations) Update(ctx context.Context, organization string, op } u := fmt.Sprintf("admin/organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } org := &AdminOrganization{} - err = s.client.do(ctx, req, org) + err = req.Do(ctx, org) if err != nil { return nil, err } @@ -199,12 +201,12 @@ func (s *adminOrganizations) UpdateModuleConsumers(ctx context.Context, organiza organizations = append(organizations, &AdminOrganizationID{ID: id}) } - req, err := s.client.newRequest("PATCH", u, organizations) + req, err := s.client.NewRequest("PATCH", u, organizations) if err != nil { return err } - err = s.client.do(ctx, req, nil) + err = req.Do(ctx, nil) if err != nil { return err } @@ -219,12 +221,12 @@ func (s *adminOrganizations) Delete(ctx context.Context, organization string) er } u := fmt.Sprintf("admin/organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *AdminOrganizationListOptions) valid() error { diff --git a/admin_organization_integration_test.go b/admin_organization_integration_test.go index bd326d94d..33326c755 100644 --- a/admin_organization_integration_test.go +++ b/admin_organization_integration_test.go @@ -39,7 +39,7 @@ func TestAdminOrganizations_List(t *testing.T) { adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{ Query: org.Name, }) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, true, adminOrgItemsContainsName(adminOrgList.Items, org.Name)) assert.Equal(t, 1, adminOrgList.CurrentPage) assert.Equal(t, 1, adminOrgList.TotalCount) @@ -51,7 +51,7 @@ func TestAdminOrganizations_List(t *testing.T) { adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{ Query: randomName, }) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, false, adminOrgItemsContainsName(adminOrgList.Items, org.Name)) assert.Equal(t, 1, adminOrgList.CurrentPage) assert.Equal(t, 0, adminOrgList.TotalCount) @@ -61,9 +61,9 @@ func TestAdminOrganizations_List(t *testing.T) { adminOrgList, err := client.Admin.Organizations.List(ctx, &AdminOrganizationListOptions{ Include: []AdminOrgIncludeOpt{AdminOrgOwners}, }) - assert.NoError(t, err) + require.NoError(t, err) - assert.NotEmpty(t, adminOrgList.Items) + require.NotEmpty(t, adminOrgList.Items) assert.NotNil(t, adminOrgList.Items[0].Owners) assert.NotEmpty(t, adminOrgList.Items[0].Owners[0].Email) }) @@ -95,8 +95,8 @@ func TestAdminOrganizations_Read(t *testing.T) { defer orgTestCleanup() adminOrg, err := client.Admin.Organizations.Read(ctx, org.Name) - assert.NoError(t, err) - assert.NotNilf(t, adminOrg, "Organization is not nil") + require.NoError(t, err) + require.NotNilf(t, adminOrg, "Organization is not nil") assert.Equal(t, adminOrg.Name, org.Name) // attributes part of an AdminOrganization response that are not null @@ -106,6 +106,7 @@ func TestAdminOrganizations_Read(t *testing.T) { assert.NotNilf(t, adminOrg.NotificationEmail, "NotificationEmail is not nil") assert.NotNilf(t, adminOrg.SsoEnabled, "SsoEnabled is not nil") assert.NotNilf(t, adminOrg.TerraformWorkerSudoEnabled, "TerraformWorkerSudoEnabledis not nil") + assert.Nilf(t, adminOrg.WorkspaceLimit, "WorkspaceLimit is nil") }) } @@ -132,12 +133,12 @@ func TestAdminOrganizations_Delete(t *testing.T) { originalOrg, _ := createOrganization(t, client) adminOrg, err := client.Admin.Organizations.Read(ctx, originalOrg.Name) - assert.NoError(t, err) - assert.NotNilf(t, adminOrg, "Organization is not nil") + require.NoError(t, err) + require.NotNil(t, adminOrg) assert.Equal(t, adminOrg.Name, originalOrg.Name) err = client.Admin.Organizations.Delete(ctx, adminOrg.Name) - assert.NoError(t, err) + require.NoError(t, err) // Cannot find deleted org _, err = client.Admin.Organizations.Read(ctx, originalOrg.Name) @@ -168,7 +169,7 @@ func TestAdminOrganizations_ModuleConsumers(t *testing.T) { defer org2TestCleanup() err := client.Admin.Organizations.UpdateModuleConsumers(ctx, org1.Name, []string{org2.Name}) - assert.NoError(t, err) + require.NoError(t, err) adminModuleConsumerList, err := client.Admin.Organizations.ListModuleConsumers(ctx, org1.Name, nil) require.NoError(t, err) @@ -180,7 +181,7 @@ func TestAdminOrganizations_ModuleConsumers(t *testing.T) { defer org3TestCleanup() err = client.Admin.Organizations.UpdateModuleConsumers(ctx, org1.Name, []string{org3.Name}) - assert.NoError(t, err) + require.NoError(t, err) adminModuleConsumerList, err = client.Admin.Organizations.ListModuleConsumers(ctx, org1.Name, nil) require.NoError(t, err) @@ -214,8 +215,8 @@ func TestAdminOrganizations_Update(t *testing.T) { defer orgTestCleanup() adminOrg, err := client.Admin.Organizations.Read(ctx, org.Name) - assert.NoError(t, err) - assert.NotNilf(t, adminOrg, "Org returned as nil") + require.NoError(t, err) + require.NotNilf(t, adminOrg, "Org returned as nil") accessBetaTools := true globalModuleSharing := false @@ -234,43 +235,51 @@ func TestAdminOrganizations_Update(t *testing.T) { } adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts) - assert.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") - assert.NoError(t, err) + require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") + require.NoError(t, err) assert.Equal(t, accessBetaTools, adminOrg.AccessBetaTools) - assert.Equal(t, globalModuleSharing, adminOrg.GlobalModuleSharing) + assert.Equal(t, adminOrg.GlobalModuleSharing, &globalModuleSharing) assert.Equal(t, isDisabled, adminOrg.IsDisabled) assert.Equal(t, terraformBuildWorkerApplyTimeout, adminOrg.TerraformBuildWorkerApplyTimeout) assert.Equal(t, terraformBuildWorkerPlanTimeout, adminOrg.TerraformBuildWorkerPlanTimeout) assert.Equal(t, terraformWorkerSudoEnabled, adminOrg.TerraformWorkerSudoEnabled) + assert.Nil(t, adminOrg.WorkspaceLimit, "default workspace limit should be nil") isDisabled = true globalModuleSharing = true + workspaceLimit := 42 opts = AdminOrganizationUpdateOptions{ GlobalModuleSharing: &globalModuleSharing, IsDisabled: &isDisabled, + WorkspaceLimit: &workspaceLimit, } adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts) - assert.NoError(t, err) - assert.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") + require.NoError(t, err) + require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") - assert.Equal(t, adminOrg.GlobalModuleSharing, globalModuleSharing) + assert.Equal(t, adminOrg.GlobalModuleSharing, &globalModuleSharing) assert.Equal(t, adminOrg.IsDisabled, isDisabled) + assert.Equal(t, &workspaceLimit, adminOrg.WorkspaceLimit) globalModuleSharing = false isDisabled = false + workspaceLimit = 0 opts = AdminOrganizationUpdateOptions{ GlobalModuleSharing: &globalModuleSharing, IsDisabled: &isDisabled, + WorkspaceLimit: &workspaceLimit, } adminOrg, err = client.Admin.Organizations.Update(ctx, org.Name, opts) - assert.NoError(t, err) - assert.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") + require.NoError(t, err) + require.NotNilf(t, adminOrg, "Org returned as nil when it shouldn't be.") - assert.Equal(t, adminOrg.GlobalModuleSharing, globalModuleSharing) + assert.Equal(t, &globalModuleSharing, adminOrg.GlobalModuleSharing) assert.Equal(t, adminOrg.IsDisabled, isDisabled) + + assert.Equal(t, &workspaceLimit, adminOrg.WorkspaceLimit) }) } diff --git a/admin_run.go b/admin_run.go index b0467aa67..6d4a65709 100644 --- a/admin_run.go +++ b/admin_run.go @@ -78,13 +78,13 @@ func (s *adminRuns) List(ctx context.Context, options *AdminRunsListOptions) (*A } u := "admin/runs" - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } rl := &AdminRunsList{} - err = s.client.do(ctx, req, rl) + err = req.Do(ctx, rl) if err != nil { return nil, err } @@ -107,12 +107,12 @@ func (s *adminRuns) ForceCancel(ctx context.Context, runID string, options Admin } u := fmt.Sprintf("admin/runs/%s/actions/force-cancel", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *AdminRunsListOptions) valid() error { diff --git a/admin_run_integration_test.go b/admin_run_integration_test.go index 5bd2b884a..ed621a00d 100644 --- a/admin_run_integration_test.go +++ b/admin_run_integration_test.go @@ -37,7 +37,7 @@ func TestAdminRuns_List(t *testing.T) { rl, err := client.Admin.Runs.List(ctx, nil) require.NoError(t, err) - assert.NotEmpty(t, rl.Items) + require.NotEmpty(t, rl.Items) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true) }) @@ -61,7 +61,7 @@ func TestAdminRuns_List(t *testing.T) { }, }) require.NoError(t, err) - assert.NotEmpty(t, rl.Items) + require.NotEmpty(t, rl.Items) assert.Equal(t, 1, rl.CurrentPage) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true) @@ -71,11 +71,10 @@ func TestAdminRuns_List(t *testing.T) { rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{ Include: []AdminRunIncludeOpt{AdminRunWorkspace}, }) + require.NoError(t, err) - assert.NoError(t, err) - - assert.NotEmpty(t, rl.Items) - assert.NotNil(t, rl.Items[0].Workspace) + require.NotEmpty(t, rl.Items) + require.NotNil(t, rl.Items[0].Workspace) assert.NotEmpty(t, rl.Items[0].Workspace.Name) }) @@ -84,11 +83,11 @@ func TestAdminRuns_List(t *testing.T) { Include: []AdminRunIncludeOpt{AdminRunWorkspaceOrg}, }) - assert.NoError(t, err) + require.NoError(t, err) + require.NotEmpty(t, rl.Items) - assert.NotEmpty(t, rl.Items) - assert.NotNil(t, rl.Items[0].Workspace) - assert.NotNil(t, rl.Items[0].Workspace.Organization) + require.NotNil(t, rl.Items[0].Workspace) + require.NotNil(t, rl.Items[0].Workspace.Organization) assert.NotEmpty(t, rl.Items[0].Workspace.Organization.Name) }) @@ -102,16 +101,16 @@ func TestAdminRuns_List(t *testing.T) { t.Run("with RunStatus.pending filter", func(t *testing.T) { r1, err := client.Runs.Read(ctx, rTest1.ID) - assert.NoError(t, err) + require.NoError(t, err) r2, err := client.Runs.Read(ctx, rTest2.ID) - assert.NoError(t, err) + require.NoError(t, err) // There should be pending Runs rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{ RunStatus: string(RunPending), }) - assert.NoError(t, err) - assert.NotEmpty(t, rl.Items) + require.NoError(t, err) + require.NotEmpty(t, rl.Items) assert.Equal(t, r1.Status, RunPlanning) assert.Equal(t, adminRunItemsContainsID(rl.Items, r1.ID), false) @@ -124,7 +123,7 @@ func TestAdminRuns_List(t *testing.T) { rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{ RunStatus: string(RunApplied), }) - assert.NoError(t, err) + require.NoError(t, err) assert.Empty(t, rl.Items) }) @@ -132,18 +131,18 @@ func TestAdminRuns_List(t *testing.T) { rl, err := client.Admin.Runs.List(ctx, &AdminRunsListOptions{ Query: rTest1.ID, }) - assert.NoError(t, err) + require.NoError(t, err) - assert.NotEmpty(t, rl.Items) + require.NotEmpty(t, rl.Items) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), true) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), false) rl, err = client.Admin.Runs.List(ctx, &AdminRunsListOptions{ Query: rTest2.ID, }) - assert.NoError(t, err) + require.NoError(t, err) - assert.NotEmpty(t, rl.Items) + require.NotEmpty(t, rl.Items) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest1.ID), false) assert.Equal(t, adminRunItemsContainsID(rl.Items, rTest2.ID), true) }) @@ -191,12 +190,18 @@ func TestAdminRuns_ForceCancel(t *testing.T) { rTestPlanning, err := client.Runs.Read(ctx, rTest1.ID) require.NoError(t, err) assert.Equal(t, RunPlanning, rTestPlanning.Status) + + require.NotNil(t, rTestPlanning.Actions) + require.NotNil(t, rTestPlanning.Permissions) assert.Equal(t, true, rTestPlanning.Actions.IsCancelable) assert.Equal(t, true, rTestPlanning.Permissions.CanForceCancel) rTestPending, err := client.Runs.Read(ctx, rTest2.ID) require.NoError(t, err) assert.Equal(t, RunPending, rTestPending.Status) + + require.NotNil(t, rTestPlanning.Actions) + require.NotNil(t, rTestPlanning.Permissions) assert.Equal(t, true, rTestPending.Actions.IsCancelable) assert.Equal(t, true, rTestPending.Permissions.CanForceCancel) @@ -231,7 +236,7 @@ func TestAdminRuns_AdminRunsListOptions_valid(t *testing.T) { } err := opts.valid() - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("has invalid status", func(t *testing.T) { @@ -259,7 +264,7 @@ func TestAdminRuns_AdminRunsListOptions_valid(t *testing.T) { } err := opts.valid() - assert.NoError(t, err) + require.NoError(t, err) }) } diff --git a/admin_setting_cost_estimation.go b/admin_setting_cost_estimation.go index e291a25f0..a9bd2a7fe 100644 --- a/admin_setting_cost_estimation.go +++ b/admin_setting_cost_estimation.go @@ -54,13 +54,13 @@ type AdminCostEstimationSettingOptions struct { // Read returns the cost estimation settings. func (a *adminCostEstimationSettings) Read(ctx context.Context) (*AdminCostEstimationSetting, error) { - req, err := a.client.newRequest("GET", "admin/cost-estimation-settings", nil) + req, err := a.client.NewRequest("GET", "admin/cost-estimation-settings", nil) if err != nil { return nil, err } ace := &AdminCostEstimationSetting{} - err = a.client.do(ctx, req, ace) + err = req.Do(ctx, ace) if err != nil { return nil, err } @@ -70,13 +70,13 @@ func (a *adminCostEstimationSettings) Read(ctx context.Context) (*AdminCostEstim // Update updates the cost-estimation settings. func (a *adminCostEstimationSettings) Update(ctx context.Context, options AdminCostEstimationSettingOptions) (*AdminCostEstimationSetting, error) { - req, err := a.client.newRequest("PATCH", "admin/cost-estimation-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/cost-estimation-settings", &options) if err != nil { return nil, err } ace := &AdminCostEstimationSetting{} - err = a.client.do(ctx, req, ace) + err = req.Do(ctx, ace) if err != nil { return nil, err } diff --git a/admin_setting_customization.go b/admin_setting_customization.go index d429a5e99..5c869c9f3 100644 --- a/admin_setting_customization.go +++ b/admin_setting_customization.go @@ -33,13 +33,13 @@ type AdminCustomizationSetting struct { // Read returns the Customization settings. func (a *adminCustomizationSettings) Read(ctx context.Context) (*AdminCustomizationSetting, error) { - req, err := a.client.newRequest("GET", "admin/customization-settings", nil) + req, err := a.client.NewRequest("GET", "admin/customization-settings", nil) if err != nil { return nil, err } cs := &AdminCustomizationSetting{} - err = a.client.do(ctx, req, cs) + err = req.Do(ctx, cs) if err != nil { return nil, err } @@ -60,13 +60,13 @@ type AdminCustomizationSettingsUpdateOptions struct { // Update updates the customization settings. func (a *adminCustomizationSettings) Update(ctx context.Context, options AdminCustomizationSettingsUpdateOptions) (*AdminCustomizationSetting, error) { - req, err := a.client.newRequest("PATCH", "admin/customization-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/customization-settings", &options) if err != nil { return nil, err } cs := &AdminCustomizationSetting{} - err = a.client.do(ctx, req, cs) + err = req.Do(ctx, cs) if err != nil { return nil, err } diff --git a/admin_setting_general.go b/admin_setting_general.go index 31124fd6b..2dc4827f8 100644 --- a/admin_setting_general.go +++ b/admin_setting_general.go @@ -54,13 +54,13 @@ type AdminGeneralSettingsUpdateOptions struct { // Read returns the general settings. func (a *adminGeneralSettings) Read(ctx context.Context) (*AdminGeneralSetting, error) { - req, err := a.client.newRequest("GET", "admin/general-settings", nil) + req, err := a.client.NewRequest("GET", "admin/general-settings", nil) if err != nil { return nil, err } ags := &AdminGeneralSetting{} - err = a.client.do(ctx, req, ags) + err = req.Do(ctx, ags) if err != nil { return nil, err } @@ -70,13 +70,13 @@ func (a *adminGeneralSettings) Read(ctx context.Context) (*AdminGeneralSetting, // Update updates the general settings. func (a *adminGeneralSettings) Update(ctx context.Context, options AdminGeneralSettingsUpdateOptions) (*AdminGeneralSetting, error) { - req, err := a.client.newRequest("PATCH", "admin/general-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/general-settings", &options) if err != nil { return nil, err } ags := &AdminGeneralSetting{} - err = a.client.do(ctx, req, ags) + err = req.Do(ctx, ags) if err != nil { return nil, err } diff --git a/admin_setting_saml.go b/admin_setting_saml.go index 8ea59aed1..2aa857f92 100644 --- a/admin_setting_saml.go +++ b/admin_setting_saml.go @@ -50,13 +50,13 @@ type AdminSAMLSetting struct { // Read returns the SAML settings. func (a *adminSAMLSettings) Read(ctx context.Context) (*AdminSAMLSetting, error) { - req, err := a.client.newRequest("GET", "admin/saml-settings", nil) + req, err := a.client.NewRequest("GET", "admin/saml-settings", nil) if err != nil { return nil, err } saml := &AdminSAMLSetting{} - err = a.client.do(ctx, req, saml) + err = req.Do(ctx, saml) if err != nil { return nil, err } @@ -82,13 +82,13 @@ type AdminSAMLSettingsUpdateOptions struct { // Update updates the SAML settings. func (a *adminSAMLSettings) Update(ctx context.Context, options AdminSAMLSettingsUpdateOptions) (*AdminSAMLSetting, error) { - req, err := a.client.newRequest("PATCH", "admin/saml-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/saml-settings", &options) if err != nil { return nil, err } saml := &AdminSAMLSetting{} - err = a.client.do(ctx, req, saml) + err = req.Do(ctx, saml) if err != nil { return nil, err } @@ -99,13 +99,13 @@ func (a *adminSAMLSettings) Update(ctx context.Context, options AdminSAMLSetting // RevokeIdpCert revokes the older IdP certificate when the new IdP // certificate is known to be functioning correctly. func (a *adminSAMLSettings) RevokeIdpCert(ctx context.Context) (*AdminSAMLSetting, error) { - req, err := a.client.newRequest("POST", "admin/saml-settings/actions/revoke-old-certificate", nil) + req, err := a.client.NewRequest("POST", "admin/saml-settings/actions/revoke-old-certificate", nil) if err != nil { return nil, err } saml := &AdminSAMLSetting{} - err = a.client.do(ctx, req, saml) + err = req.Do(ctx, saml) if err != nil { return nil, err } diff --git a/admin_setting_smtp.go b/admin_setting_smtp.go index 4cb6046ed..8f04c5a6d 100644 --- a/admin_setting_smtp.go +++ b/admin_setting_smtp.go @@ -43,13 +43,13 @@ type AdminSMTPSetting struct { // Read returns the SMTP settings. func (a *adminSMTPSettings) Read(ctx context.Context) (*AdminSMTPSetting, error) { - req, err := a.client.newRequest("GET", "admin/smtp-settings", nil) + req, err := a.client.NewRequest("GET", "admin/smtp-settings", nil) if err != nil { return nil, err } smtp := &AdminSMTPSetting{} - err = a.client.do(ctx, req, smtp) + err = req.Do(ctx, smtp) if err != nil { return nil, err } @@ -77,13 +77,13 @@ func (a *adminSMTPSettings) Update(ctx context.Context, options AdminSMTPSetting return nil, err } - req, err := a.client.newRequest("PATCH", "admin/smtp-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/smtp-settings", &options) if err != nil { return nil, err } smtp := &AdminSMTPSetting{} - err = a.client.do(ctx, req, smtp) + err = req.Do(ctx, smtp) if err != nil { return nil, err } diff --git a/admin_setting_twilio.go b/admin_setting_twilio.go index 6913f2048..5593bd0ff 100644 --- a/admin_setting_twilio.go +++ b/admin_setting_twilio.go @@ -34,13 +34,13 @@ type AdminTwilioSetting struct { // Read returns the Twilio settings. func (a *adminTwilioSettings) Read(ctx context.Context) (*AdminTwilioSetting, error) { - req, err := a.client.newRequest("GET", "admin/twilio-settings", nil) + req, err := a.client.NewRequest("GET", "admin/twilio-settings", nil) if err != nil { return nil, err } twilio := &AdminTwilioSetting{} - err = a.client.do(ctx, req, twilio) + err = req.Do(ctx, twilio) if err != nil { return nil, err } @@ -66,13 +66,13 @@ type AdminTwilioSettingsVerifyOptions struct { // Update updates the Twilio settings. func (a *adminTwilioSettings) Update(ctx context.Context, options AdminTwilioSettingsUpdateOptions) (*AdminTwilioSetting, error) { - req, err := a.client.newRequest("PATCH", "admin/twilio-settings", &options) + req, err := a.client.NewRequest("PATCH", "admin/twilio-settings", &options) if err != nil { return nil, err } twilio := &AdminTwilioSetting{} - err = a.client.do(ctx, req, twilio) + err = req.Do(ctx, twilio) if err != nil { return nil, err } @@ -85,12 +85,12 @@ func (a *adminTwilioSettings) Verify(ctx context.Context, options AdminTwilioSet if err := options.valid(); err != nil { return err } - req, err := a.client.newRequest("PATCH", "admin/twilio-settings/verify", &options) + req, err := a.client.NewRequest("PATCH", "admin/twilio-settings/verify", &options) if err != nil { return err } - return a.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o AdminTwilioSettingsVerifyOptions) valid() error { diff --git a/admin_terraform_version.go b/admin_terraform_version.go index 10eb9e12e..b2c92de9e 100644 --- a/admin_terraform_version.go +++ b/admin_terraform_version.go @@ -100,13 +100,13 @@ type AdminTerraformVersionsList struct { // List all the terraform versions. func (a *adminTerraformVersions) List(ctx context.Context, options *AdminTerraformVersionsListOptions) (*AdminTerraformVersionsList, error) { - req, err := a.client.newRequest("GET", "admin/terraform-versions", options) + req, err := a.client.NewRequest("GET", "admin/terraform-versions", options) if err != nil { return nil, err } tvl := &AdminTerraformVersionsList{} - err = a.client.do(ctx, req, tvl) + err = req.Do(ctx, tvl) if err != nil { return nil, err } @@ -121,13 +121,13 @@ func (a *adminTerraformVersions) Read(ctx context.Context, id string) (*AdminTer } u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id)) - req, err := a.client.newRequest("GET", u, nil) + req, err := a.client.NewRequest("GET", u, nil) if err != nil { return nil, err } tfv := &AdminTerraformVersion{} - err = a.client.do(ctx, req, tfv) + err = req.Do(ctx, tfv) if err != nil { return nil, err } @@ -140,13 +140,13 @@ func (a *adminTerraformVersions) Create(ctx context.Context, options AdminTerraf if err := options.valid(); err != nil { return nil, err } - req, err := a.client.newRequest("POST", "admin/terraform-versions", &options) + req, err := a.client.NewRequest("POST", "admin/terraform-versions", &options) if err != nil { return nil, err } tfv := &AdminTerraformVersion{} - err = a.client.do(ctx, req, tfv) + err = req.Do(ctx, tfv) if err != nil { return nil, err } @@ -161,13 +161,13 @@ func (a *adminTerraformVersions) Update(ctx context.Context, id string, options } u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id)) - req, err := a.client.newRequest("PATCH", u, &options) + req, err := a.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } tfv := &AdminTerraformVersion{} - err = a.client.do(ctx, req, tfv) + err = req.Do(ctx, tfv) if err != nil { return nil, err } @@ -182,12 +182,12 @@ func (a *adminTerraformVersions) Delete(ctx context.Context, id string) error { } u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id)) - req, err := a.client.newRequest("DELETE", u, nil) + req, err := a.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return a.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o AdminTerraformVersionCreateOptions) valid() error { diff --git a/admin_user.go b/admin_user.go index 80fc6ab71..c955bf56d 100644 --- a/admin_user.go +++ b/admin_user.go @@ -96,13 +96,13 @@ func (a *adminUsers) List(ctx context.Context, options *AdminUserListOptions) (* } u := "admin/users" - req, err := a.client.newRequest("GET", u, options) + req, err := a.client.NewRequest("GET", u, options) if err != nil { return nil, err } aul := &AdminUserList{} - err = a.client.do(ctx, req, aul) + err = req.Do(ctx, aul) if err != nil { return nil, err } @@ -117,12 +117,12 @@ func (a *adminUsers) Delete(ctx context.Context, userID string) error { } u := fmt.Sprintf("admin/users/%s", url.QueryEscape(userID)) - req, err := a.client.newRequest("DELETE", u, nil) + req, err := a.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return a.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Suspend a user by its ID. @@ -132,13 +132,13 @@ func (a *adminUsers) Suspend(ctx context.Context, userID string) (*AdminUser, er } u := fmt.Sprintf("admin/users/%s/actions/suspend", url.QueryEscape(userID)) - req, err := a.client.newRequest("POST", u, nil) + req, err := a.client.NewRequest("POST", u, nil) if err != nil { return nil, err } au := &AdminUser{} - err = a.client.do(ctx, req, au) + err = req.Do(ctx, au) if err != nil { return nil, err } @@ -153,13 +153,13 @@ func (a *adminUsers) Unsuspend(ctx context.Context, userID string) (*AdminUser, } u := fmt.Sprintf("admin/users/%s/actions/unsuspend", url.QueryEscape(userID)) - req, err := a.client.newRequest("POST", u, nil) + req, err := a.client.NewRequest("POST", u, nil) if err != nil { return nil, err } au := &AdminUser{} - err = a.client.do(ctx, req, au) + err = req.Do(ctx, au) if err != nil { return nil, err } @@ -174,13 +174,13 @@ func (a *adminUsers) GrantAdmin(ctx context.Context, userID string) (*AdminUser, } u := fmt.Sprintf("admin/users/%s/actions/grant_admin", url.QueryEscape(userID)) - req, err := a.client.newRequest("POST", u, nil) + req, err := a.client.NewRequest("POST", u, nil) if err != nil { return nil, err } au := &AdminUser{} - err = a.client.do(ctx, req, au) + err = req.Do(ctx, au) if err != nil { return nil, err } @@ -195,13 +195,13 @@ func (a *adminUsers) RevokeAdmin(ctx context.Context, userID string) (*AdminUser } u := fmt.Sprintf("admin/users/%s/actions/revoke_admin", url.QueryEscape(userID)) - req, err := a.client.newRequest("POST", u, nil) + req, err := a.client.NewRequest("POST", u, nil) if err != nil { return nil, err } au := &AdminUser{} - err = a.client.do(ctx, req, au) + err = req.Do(ctx, au) if err != nil { return nil, err } @@ -217,13 +217,13 @@ func (a *adminUsers) Disable2FA(ctx context.Context, userID string) (*AdminUser, } u := fmt.Sprintf("admin/users/%s/actions/disable_two_factor", url.QueryEscape(userID)) - req, err := a.client.newRequest("POST", u, nil) + req, err := a.client.NewRequest("POST", u, nil) if err != nil { return nil, err } au := &AdminUser{} - err = a.client.do(ctx, req, au) + err = req.Do(ctx, au) if err != nil { return nil, err } diff --git a/admin_user_integration_test.go b/admin_user_integration_test.go index 3ec021037..70e9d5d36 100644 --- a/admin_user_integration_test.go +++ b/admin_user_integration_test.go @@ -18,7 +18,7 @@ func TestAdminUsers_List(t *testing.T) { ctx := context.Background() currentUser, err := client.Users.ReadCurrent(ctx) - assert.NoError(t, err) + require.NoError(t, err) org, orgCleanup := createOrganization(t, client) defer orgCleanup() @@ -79,10 +79,10 @@ func TestAdminUsers_List(t *testing.T) { Include: []AdminUserIncludeOpt{AdminUserOrgs}, }) - assert.NoError(t, err) + require.NoError(t, err) + require.NotEmpty(t, ul.Items) + require.NotEmpty(t, ul.Items[0].Organizations) - assert.NotEmpty(t, ul.Items) - assert.NotNil(t, ul.Items[0].Organizations) assert.NotEmpty(t, ul.Items[0].Organizations[0].Name) }) @@ -91,9 +91,9 @@ func TestAdminUsers_List(t *testing.T) { Administrators: "true", }) - assert.NoError(t, err) - assert.NotEmpty(t, ul.Items) - assert.NotNil(t, ul.Items[0]) + require.NoError(t, err) + require.NotEmpty(t, ul.Items) + require.NotNil(t, ul.Items[0]) // We use this `includesEmail` helper function because throughout // the tests, there could be multiple admins, depending on the // ordering of the test runs. diff --git a/admin_workspace.go b/admin_workspace.go index 1fcfc90d1..b5e9b98e1 100644 --- a/admin_workspace.go +++ b/admin_workspace.go @@ -81,13 +81,13 @@ func (s *adminWorkspaces) List(ctx context.Context, options *AdminWorkspaceListO } u := "admin/workspaces" - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } awl := &AdminWorkspaceList{} - err = s.client.do(ctx, req, awl) + err = req.Do(ctx, awl) if err != nil { return nil, err } @@ -102,13 +102,13 @@ func (s *adminWorkspaces) Read(ctx context.Context, workspaceID string) (*AdminW } u := fmt.Sprintf("admin/workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } aw := &AdminWorkspace{} - err = s.client.do(ctx, req, aw) + err = req.Do(ctx, aw) if err != nil { return nil, err } @@ -123,12 +123,12 @@ func (s *adminWorkspaces) Delete(ctx context.Context, workspaceID string) error } u := fmt.Sprintf("admin/workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *AdminWorkspaceListOptions) valid() error { diff --git a/admin_workspace_integration_test.go b/admin_workspace_integration_test.go index dfcf0437b..8b0c357f7 100644 --- a/admin_workspace_integration_test.go +++ b/admin_workspace_integration_test.go @@ -91,9 +91,9 @@ func TestAdminWorkspaces_List(t *testing.T) { Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceOrg}, }) - assert.NoError(t, err) - assert.NotEmpty(t, wl.Items) - assert.NotNil(t, wl.Items[0].Organization) + require.NoError(t, err) + require.NotEmpty(t, wl.Items) + require.NotNil(t, wl.Items[0].Organization) assert.NotEmpty(t, wl.Items[0].Organization.Name) }) @@ -106,16 +106,16 @@ func TestAdminWorkspaces_List(t *testing.T) { Workspace: wTest1, } run, err := client.Runs.Create(ctx, runOpts) - assert.NoError(t, err) + require.NoError(t, err) wl, err := client.Admin.Workspaces.List(ctx, &AdminWorkspaceListOptions{ Include: []AdminWorkspaceIncludeOpt{AdminWorkspaceCurrentRun}, }) - assert.NoError(t, err) + require.NoError(t, err) - assert.NotEmpty(t, wl.Items) - assert.NotNil(t, wl.Items[0].CurrentRun) + require.NotEmpty(t, wl.Items) + require.NotNil(t, wl.Items[0].CurrentRun) assert.Equal(t, wl.Items[0].CurrentRun.ID, run.ID) }) } @@ -149,8 +149,8 @@ func TestAdminWorkspaces_Read(t *testing.T) { defer workspaceCleanup() adminWorkspace, err := client.Admin.Workspaces.Read(ctx, workspace.ID) - assert.NoError(t, err) - assert.NotNilf(t, adminWorkspace, "Admin Workspace is not nil") + require.NoError(t, err) + require.NotNilf(t, adminWorkspace, "Admin Workspace is not nil") assert.Equal(t, adminWorkspace.ID, workspace.ID) assert.Equal(t, adminWorkspace.Name, workspace.Name) assert.Equal(t, adminWorkspace.Locked, workspace.Locked) @@ -183,12 +183,12 @@ func TestAdminWorkspaces_Delete(t *testing.T) { workspace, _ := createWorkspace(t, client, org) adminWorkspace, err := client.Admin.Workspaces.Read(ctx, workspace.ID) - assert.NoError(t, err) - assert.NotNilf(t, adminWorkspace, "Admin Workspace is not nil") + require.NoError(t, err) + require.NotNilf(t, adminWorkspace, "Admin Workspace is not nil") assert.Equal(t, adminWorkspace.ID, workspace.ID) err = client.Admin.Workspaces.Delete(ctx, adminWorkspace.ID) - assert.NoError(t, err) + require.NoError(t, err) // Cannot find deleted workspace _, err = client.Admin.Workspaces.Read(ctx, workspace.ID) diff --git a/agent.go b/agent.go new file mode 100644 index 000000000..f0760bba6 --- /dev/null +++ b/agent.go @@ -0,0 +1,173 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ Agents = (*agents)(nil) + +// Agents describes all the agent-related methods that the +// Terraform Cloud API supports. +// TFE API docs: https://www.terraform.io/docs/cloud/api/agents.html +type Agents interface { + // Read an agent by its ID. + Read(ctx context.Context, agentID string) (*Agent, error) + + // Read an agent by its ID with the given options. + ReadWithOptions(ctx context.Context, agentID string, options *AgentReadOptions) (*Agent, error) + + // List all the agents of the given pool. + List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error) + + // Delete an agent by its ID. + Delete(ctx context.Context, agentPoolID string) error +} + +// agents implements Agents. +type agents struct { + client *Client +} + +// AgentList represents a list of agents. +type AgentList struct { + *Pagination + Items []*Agent +} + +// Agent represents a Terraform Cloud agent. +type Agent struct { + ID string `jsonapi:"primary,agents"` + Name string `jsonapi:"attr,name"` + IP string `jsonapi:"attr,ip-address"` + + // Relations + Organization *Organization `jsonapi:"relation,organization"` + Workspaces []*Workspace `jsonapi:"relation,workspaces"` +} + +// A list of relations to include +// https://www.terraform.io/cloud-docs/api-docs/agents#available-related-resources +type AgentIncludeOpt string + +const ( + AgentWorkspaces AgentIncludeOpt = "workspaces" +) + +// AgentReadOptions represents the options for reading an agent. +type AgentReadOptions struct { + Include []AgentIncludeOpt `url:"include,omitempty"` +} + +// AgentListOptions represents the options for listing agents. +type AgentListOptions struct { + ListOptions + // Optional: A list of relations to include. See available resources + // https://www.terraform.io/cloud-docs/api-docs/agents#available-related-resources + Include []AgentIncludeOpt `url:"include,omitempty"` +} + +// Read a single agent by its ID +func (s *agents) Read(ctx context.Context, agentID string) (*Agent, error) { + return s.ReadWithOptions(ctx, agentID, nil) +} + +// Read a single agent by its ID with options. +func (s *agents) ReadWithOptions(ctx context.Context, agentID string, options *AgentReadOptions) (*Agent, error) { + if !validStringID(&agentID) { + return nil, ErrInvalidAgentID //undeclared var name + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("agents/%s", url.QueryEscape(agentID)) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + agent := &Agent{} + err = req.Do(ctx, agent) + if err != nil { + return nil, err + } + + return agent, nil //cannot use agent as *Agent value in return statement +} + +// List all the agents of the given organization. +func (s *agents) List(ctx context.Context, agentPoolID string, options *AgentListOptions) (*AgentList, error) { + if !validStringID(&agentPoolID) { + return nil, ErrInvalidOrg + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("agent-pools/%s/agents", url.QueryEscape(agentPoolID)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + agentList := &AgentList{} + err = req.Do(ctx, agentList) + if err != nil { + return nil, err + } + + return agentList, nil +} + +// Delete an agent by its ID. +func (s *agents) Delete(ctx context.Context, agentID string) error { + if !validStringID(&agentID) { + return ErrInvalidAgentID + } + + u := fmt.Sprintf("agents/%s", url.QueryEscape(agentID)) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return err + } + + return req.Do(ctx, nil) +} + +func (o *AgentReadOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + if err := validateAgentIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func (o *AgentListOptions) valid() error { + if o == nil { + return nil // nothing to validate + } + if err := validateAgentIncludeParams(o.Include); err != nil { + return err + } + + return nil +} + +func validateAgentIncludeParams(params []AgentIncludeOpt) error { + for _, p := range params { + switch p { + case AgentWorkspaces: + // do nothing + default: + return ErrInvalidIncludeValue + } + } + + return nil +} diff --git a/agent_integration_test.go b/agent_integration_test.go new file mode 100644 index 000000000..7ffa545f0 --- /dev/null +++ b/agent_integration_test.go @@ -0,0 +1,114 @@ +//go:build integration +// +build integration + +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAgentsRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + agent, agentCleanup := createAgent(t, client, nil, nil, nil) + defer agentCleanup() + t.Log("log agent: ", agent) + // t.Log("log pool: ", pool) + + t.Run("when the agent exists", func(t *testing.T) { + k, err := client.Agents.Read(ctx, agent.ID) + require.NoError(t, err) + assert.Equal(t, agent, k) + }) + + t.Run("when the agent does not exist", func(t *testing.T) { + k, err := client.Agents.Read(ctx, "nonexisting") + assert.Nil(t, k) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("without a valid agent ID", func(t *testing.T) { + k, err := client.Agents.Read(ctx, badIdentifier) + assert.Nil(t, k) + assert.EqualError(t, err, ErrInvalidAgentID.Error()) + }) +} + +func TestAgentsList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + agent, agentCleanup := createAgent(t, client, nil, nil, nil) + defer agentCleanup() + t.Log("log agent: ", agent) + // t.Log("log pool: ", pool) + + t.Run("without list options", func(t *testing.T) { + agentList, err := client.Agents.List(ctx, agent.Organization.Name, nil) + require.NoError(t, err) + assert.Contains(t, agentList.Items, agent) + + assert.Equal(t, 1, agentList.CurrentPage) + assert.Equal(t, 1, agentList.TotalCount) + }) + + t.Run("with list options", func(t *testing.T) { + // 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. + agents, err := client.Agents.List(ctx, agent.Organization.Name, &AgentListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }) + require.NoError(t, err) + assert.Empty(t, agents.Items) + assert.Equal(t, 999, agents.CurrentPage) + assert.Equal(t, 1, agents.TotalCount) + }) + + t.Run("without a valid organization", func(t *testing.T) { + agents, err := client.Agents.List(ctx, badIdentifier, nil) + assert.Nil(t, agents) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) + +} + +func TestAgentsDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + agent, agentCleanup := createAgent(t, client, nil, nil, nil) + defer agentCleanup() + t.Log("log agent: ", agent) + // t.Log("log pool: ", pool) + + // t.Run("with valid options", func(t *testing.T) { + // err := client.Agents.Delete(ctx, agent.ID) + // //agent status cannot be idle, to be deleted + // //need to account for various statuses + // require.NoError(t, err) + + // // Try loading the agent - it should fail. + // _, err = client.Agents.Read(ctx, agent.ID) + // assert.Equal(t, err, ErrResourceNotFound) + // }) + + // t.Run("when the agent does not exist", func(t *testing.T) { + // err := client.Agents.Delete(ctx, agent.ID) + // assert.Equal(t, err, ErrResourceNotFound) + // }) + + t.Run("when the agent ID is invalid", func(t *testing.T) { + err := client.Agents.Delete(ctx, badIdentifier) + assert.EqualError(t, err, ErrInvalidAgentID.Error()) + }) + +} diff --git a/agent_pool.go b/agent_pool.go index f2913efb7..7d4ad2441 100644 --- a/agent_pool.go +++ b/agent_pool.go @@ -20,10 +20,10 @@ type AgentPools interface { // Create a new agent pool with the given options. Create(ctx context.Context, organization string, options AgentPoolCreateOptions) (*AgentPool, error) - // Read a agent pool by its ID. + // Read an agent pool by its ID. Read(ctx context.Context, agentPoolID string) (*AgentPool, error) - // Read a agent pool by its ID with the given options. + // Read an agent pool by its ID with the given options. ReadWithOptions(ctx context.Context, agentPoolID string, options *AgentPoolReadOptions) (*AgentPool, error) // Update an agent pool by its ID. @@ -46,12 +46,14 @@ type AgentPoolList struct { // AgentPool represents a Terraform Cloud agent pool. type AgentPool struct { - ID string `jsonapi:"primary,agent-pools"` - Name string `jsonapi:"attr,name"` + ID string `jsonapi:"primary,agent-pools"` + Name string `jsonapi:"attr,name"` + OrganizationScoped bool `jsonapi:"attr,organization-scoped"` // Relations - Organization *Organization `jsonapi:"relation,organization"` - Workspaces []*Workspace `jsonapi:"relation,workspaces"` + Organization *Organization `jsonapi:"relation,organization"` + Workspaces []*Workspace `jsonapi:"relation,workspaces"` + AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"` } // A list of relations to include @@ -70,6 +72,9 @@ type AgentPoolListOptions struct { // Optional: A list of relations to include. See available resources // https://www.terraform.io/cloud-docs/api-docs/agents#available-related-resources Include []AgentPoolIncludeOpt `url:"include,omitempty"` + + // Optional: A search query string used to filter agent pool. Agent pools are searchable by name + Query string `url:"q,omitempty"` } // AgentPoolCreateOptions represents the options for creating an agent pool. @@ -94,13 +99,13 @@ func (s *agentPools) List(ctx context.Context, organization string, options *Age } u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } poolList := &AgentPoolList{} - err = s.client.do(ctx, req, poolList) + err = req.Do(ctx, poolList) if err != nil { return nil, err } @@ -119,13 +124,13 @@ func (s *agentPools) Create(ctx context.Context, organization string, options Ag } u := fmt.Sprintf("organizations/%s/agent-pools", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } pool := &AgentPool{} - err = s.client.do(ctx, req, pool) + err = req.Do(ctx, pool) if err != nil { return nil, err } @@ -148,13 +153,13 @@ func (s *agentPools) ReadWithOptions(ctx context.Context, agentpoolID string, op } u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentpoolID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } pool := &AgentPool{} - err = s.client.do(ctx, req, pool) + err = req.Do(ctx, pool) if err != nil { return nil, err } @@ -172,6 +177,12 @@ type AgentPoolUpdateOptions struct { // A new name to identify the agent pool. Name *string `jsonapi:"attr,name"` + + // True if the agent pool is organization scoped, false otherwise. + OrganizationScoped *bool `jsonapi:"attr,organization-scoped,omitempty"` + + // A new list of workspaces that are associated with an agent pool. + AllowedWorkspaces []*Workspace `jsonapi:"relation,allowed-workspaces"` } // Update an agent pool by its ID. @@ -185,13 +196,13 @@ func (s *agentPools) Update(ctx context.Context, agentPoolID string, options Age } u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } k := &AgentPool{} - err = s.client.do(ctx, req, k) + err = req.Do(ctx, k) if err != nil { return nil, err } @@ -206,12 +217,12 @@ func (s *agentPools) Delete(ctx context.Context, agentPoolID string) error { } u := fmt.Sprintf("agent-pools/%s", url.QueryEscape(agentPoolID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o AgentPoolCreateOptions) valid() error { diff --git a/agent_pool_integration_test.go b/agent_pool_integration_test.go index a054ff4f9..2e32f130e 100644 --- a/agent_pool_integration_test.go +++ b/agent_pool_integration_test.go @@ -44,7 +44,9 @@ func TestAgentPoolsList(t *testing.T) { Include: []AgentPoolIncludeOpt{AgentPoolWorkspaces}, }) require.NoError(t, err) - assert.NotEmpty(t, k.Items[0].Workspaces[0]) + require.NotEmpty(t, k.Items) + require.NotEmpty(t, k.Items[0].Workspaces) + assert.NotNil(t, k.Items[0].Workspaces[0]) }) t.Run("with list options", func(t *testing.T) { @@ -68,6 +70,21 @@ func TestAgentPoolsList(t *testing.T) { assert.Nil(t, pools) assert.EqualError(t, err, ErrInvalidOrg.Error()) }) + + t.Run("with query options", func(t *testing.T) { + + pools, err := client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{ + Query: agentPool.Name, + }) + require.NoError(t, err) + assert.Equal(t, len(pools.Items), 1) + + pools, err = client.AgentPools.List(ctx, orgTest.Name, &AgentPoolListOptions{ + Query: agentPool.Name + "not_going_to_match", + }) + require.NoError(t, err) + assert.Empty(t, pools.Items) + }) } func TestAgentPoolsCreate(t *testing.T) { @@ -200,6 +217,41 @@ func TestAgentPoolsUpdate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidAgentPoolID.Error()) }) + + t.Run("when updating allowed-workspaces", func(t *testing.T) { + kBefore, kTestCleanup := createAgentPool(t, client, orgTest) + defer kTestCleanup() + + workspaceTest, workspaceTestCleanup := createWorkspace(t, client, orgTest) + defer workspaceTestCleanup() + + kAfter, err := client.AgentPools.Update(ctx, kBefore.ID, AgentPoolUpdateOptions{ + Name: String(kBefore.Name), + AllowedWorkspaces: []*Workspace{ + workspaceTest, + }, + }) + require.NoError(t, err) + + 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() + + 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 TestAgentPoolsDelete(t *testing.T) { diff --git a/agent_token.go b/agent_token.go index aa74ae3ba..f7c1ed260 100644 --- a/agent_token.go +++ b/agent_token.go @@ -68,13 +68,13 @@ func (s *agentTokens) List(ctx context.Context, agentPoolID string) (*AgentToken } u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.QueryEscape(agentPoolID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } tokenList := &AgentTokenList{} - err = s.client.do(ctx, req, tokenList) + err = req.Do(ctx, tokenList) if err != nil { return nil, err } @@ -93,13 +93,13 @@ func (s *agentTokens) Create(ctx context.Context, agentPoolID string, options Ag } u := fmt.Sprintf("agent-pools/%s/authentication-tokens", url.QueryEscape(agentPoolID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } at := &AgentToken{} - err = s.client.do(ctx, req, at) + err = req.Do(ctx, at) if err != nil { return nil, err } @@ -114,13 +114,13 @@ func (s *agentTokens) Read(ctx context.Context, agentTokenID string) (*AgentToke } u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(agentTokenID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } at := &AgentToken{} - err = s.client.do(ctx, req, at) + err = req.Do(ctx, at) if err != nil { return nil, err } @@ -135,10 +135,10 @@ func (s *agentTokens) Delete(ctx context.Context, agentTokenID string) error { } u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(agentTokenID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/agent_token_integration_test.go b/agent_token_integration_test.go index 4987a5b35..d0ab6a7ff 100644 --- a/agent_token_integration_test.go +++ b/agent_token_integration_test.go @@ -98,7 +98,7 @@ func TestAgentTokensRead(t *testing.T) { t.Run("read token with valid token ID", func(t *testing.T) { at, err := client.AgentTokens.Read(ctx, token.ID) - assert.NoError(t, err) + require.NoError(t, err) // The initial API call to create a token will return a value in the token // object. Empty that out for comparison token.Token = "" @@ -125,7 +125,7 @@ func TestAgentTokensDelete(t *testing.T) { t.Run("with valid token ID", func(t *testing.T) { err := client.AgentTokens.Delete(ctx, token.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("without valid token ID", func(t *testing.T) { diff --git a/apply.go b/apply.go index b18680f07..103153b10 100644 --- a/apply.go +++ b/apply.go @@ -72,13 +72,13 @@ func (s *applies) Read(ctx context.Context, applyID string) (*Apply, error) { } u := fmt.Sprintf("applies/%s", url.QueryEscape(applyID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } a := &Apply{} - err = s.client.do(ctx, req, a) + err = req.Do(ctx, a) if err != nil { return nil, err } diff --git a/apply_integration_test.go b/apply_integration_test.go index adc1a772d..911b7b35c 100644 --- a/apply_integration_test.go +++ b/apply_integration_test.go @@ -22,7 +22,7 @@ func TestAppliesRead(t *testing.T) { wTest, wTestCleanup := createWorkspace(t, client, nil) defer wTestCleanup() - rTest, rTestCleanup := createAppliedRun(t, client, wTest) + rTest, rTestCleanup := createRunApply(t, client, wTest) defer rTestCleanup() t.Run("when the plan exists", func(t *testing.T) { @@ -50,7 +50,7 @@ func TestAppliesLogs(t *testing.T) { client := testClient(t) ctx := context.Background() - rTest, rTestCleanup := createAppliedRun(t, client, nil) + rTest, rTestCleanup := createRunApply(t, client, nil) defer rTestCleanup() t.Run("when the log exists", func(t *testing.T) { diff --git a/audit_trail_integration_test.go b/audit_trail_integration_test.go index b0152fc16..d4d116a40 100644 --- a/audit_trail_integration_test.go +++ b/audit_trail_integration_test.go @@ -33,16 +33,16 @@ func TestAuditTrailsList(t *testing.T) { t.Run("with no specified timeframe", func(t *testing.T) { atl, err := auditTrailClient.AuditTrails.List(ctx, nil) require.NoError(t, err) - require.Greater(t, len(atl.Items), 0) + require.NotEmpty(t, atl.Items) log := atl.Items[0] assert.NotEmpty(t, log.ID) assert.NotEmpty(t, log.Timestamp) assert.NotEmpty(t, log.Type) assert.NotEmpty(t, log.Version) - assert.NotNil(t, log.Resource) - assert.NotNil(t, log.Auth) - assert.NotNil(t, log.Request) + require.NotNil(t, log.Resource) + require.NotNil(t, log.Auth) + require.NotNil(t, log.Request) t.Run("with resource deserialized correctly", func(t *testing.T) { assert.NotEmpty(t, log.Resource.ID) @@ -85,8 +85,8 @@ func TestAuditTrailsList(t *testing.T) { }) require.NoError(t, err) + require.Greater(t, len(atl.Items), 0) assert.LessOrEqual(t, len(atl.Items), 20) - assert.Greater(t, len(atl.Items), 0) for _, log := range atl.Items { assert.True(t, log.Timestamp.After(since)) diff --git a/comment.go b/comment.go index 0f1a069a8..9b8ab0973 100644 --- a/comment.go +++ b/comment.go @@ -60,13 +60,13 @@ func (s *comments) List(ctx context.Context, runID string) (*CommentList, error) } u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } cl := &CommentList{} - err = s.client.do(ctx, req, cl) + err = req.Do(ctx, cl) if err != nil { return nil, err } @@ -85,13 +85,13 @@ func (s *comments) Create(ctx context.Context, runID string, options CommentCrea } u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } comm := &Comment{} - err = s.client.do(ctx, req, comm) + err = req.Do(ctx, comm) if err != nil { return nil, err } @@ -106,13 +106,13 @@ func (s *comments) Read(ctx context.Context, commentID string) (*Comment, error) } u := fmt.Sprintf("comments/%s", url.QueryEscape(commentID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } comm := &Comment{} - err = s.client.do(ctx, req, comm) + err = req.Do(ctx, comm) if err != nil { return nil, err } diff --git a/configuration_version.go b/configuration_version.go index ad4a8a375..b02e8143a 100644 --- a/configuration_version.go +++ b/configuration_version.go @@ -74,6 +74,7 @@ const ( ConfigurationSourceBitbucket ConfigurationSource = "bitbucket" ConfigurationSourceGithub ConfigurationSource = "github" ConfigurationSourceGitlab ConfigurationSource = "gitlab" + ConfigurationSourceAdo ConfigurationSource = "ado" ConfigurationSourceTerraform ConfigurationSource = "terraform" ) @@ -92,7 +93,7 @@ type ConfigurationVersion struct { Error string `jsonapi:"attr,error"` ErrorMessage string `jsonapi:"attr,error-message"` Source ConfigurationSource `jsonapi:"attr,source"` - Speculative bool `jsonapi:"attr,speculative "` + Speculative bool `jsonapi:"attr,speculative"` Status ConfigurationStatus `jsonapi:"attr,status"` StatusTimestamps *CVStatusTimestamps `jsonapi:"attr,status-timestamps"` UploadURL string `jsonapi:"attr,upload-url"` @@ -188,13 +189,13 @@ func (s *configurationVersions) List(ctx context.Context, workspaceID string, op } u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } cvl := &ConfigurationVersionList{} - err = s.client.do(ctx, req, cvl) + err = req.Do(ctx, cvl) if err != nil { return nil, err } @@ -210,13 +211,13 @@ func (s *configurationVersions) Create(ctx context.Context, workspaceID string, } u := fmt.Sprintf("workspaces/%s/configuration-versions", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } cv := &ConfigurationVersion{} - err = s.client.do(ctx, req, cv) + err = req.Do(ctx, cv) if err != nil { return nil, err } @@ -239,13 +240,13 @@ func (s *configurationVersions) ReadWithOptions(ctx context.Context, cvID string } u := fmt.Sprintf("configuration-versions/%s", url.QueryEscape(cvID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } cv := &ConfigurationVersion{} - err = s.client.do(ctx, req, cv) + err = req.Do(ctx, cv) if err != nil { return nil, err } @@ -277,12 +278,12 @@ func (s *configurationVersions) Upload(ctx context.Context, u, path string) erro return err } - req, err := s.client.newRequest("PUT", u, body) + req, err := s.client.NewRequest("PUT", u, body) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Archive a configuration version. This can only be done on configuration versions that @@ -295,12 +296,12 @@ func (s *configurationVersions) Archive(ctx context.Context, cvID string) error body := bytes.NewBuffer(nil) u := fmt.Sprintf("configuration-versions/%s/actions/archive", url.QueryEscape(cvID)) - req, err := s.client.newRequest("POST", u, body) + req, err := s.client.NewRequest("POST", u, body) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *ConfigurationVersionReadOptions) valid() error { @@ -347,13 +348,13 @@ func (s *configurationVersions) Download(ctx context.Context, cvID string) ([]by } u := fmt.Sprintf("configuration-versions/%s/download", url.QueryEscape(cvID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } var buf bytes.Buffer - err = s.client.do(ctx, req, &buf) + err = req.Do(ctx, &buf) if err != nil { return nil, err } diff --git a/configuration_version_integration_test.go b/configuration_version_integration_test.go index e111d5fb2..de17532bd 100644 --- a/configuration_version_integration_test.go +++ b/configuration_version_integration_test.go @@ -7,10 +7,12 @@ import ( "bytes" "context" "encoding/json" - "github.com/hashicorp/go-slug" + "errors" "testing" "time" + "github.com/hashicorp/go-slug" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -155,22 +157,25 @@ func TestConfigurationVersionsReadWithOptions(t *testing.T) { wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{QueueAllRuns: Bool(true)}) defer wTestCleanup() - // Hack: Wait for TFC to ingress the configuration and queue a run - time.Sleep(3 * time.Second) + w, err := retry(func() (interface{}, error) { + w, err := client.Workspaces.ReadByIDWithOptions(ctx, wTest.ID, &WorkspaceReadOptions{ + Include: []WSIncludeOpt{WSCurrentRunConfigVer}, + }) - w, err := client.Workspaces.ReadByIDWithOptions(ctx, wTest.ID, &WorkspaceReadOptions{ - Include: []WSIncludeOpt{WSCurrentRunConfigVer}, - }) + if err != nil { + return nil, err + } - if err != nil { - require.NoError(t, err) - } + if w.CurrentRun == nil { + return nil, errors.New("A run was expected to be found on this workspace as a test pre-condition") + } - if w.CurrentRun == nil { - t.Fatal("A run was expected to be found on this workspace as a test pre-condition") - } + return w, nil + }) - cv := w.CurrentRun.ConfigurationVersion + require.NoError(t, err) + + cv := w.(*Workspace).CurrentRun.ConfigurationVersion t.Run("when the configuration version exists", func(t *testing.T) { options := &ConfigurationVersionReadOptions{ @@ -180,7 +185,7 @@ func TestConfigurationVersionsReadWithOptions(t *testing.T) { cv, err := client.ConfigurationVersions.ReadWithOptions(ctx, cv.ID, options) require.NoError(t, err) - assert.NotZero(t, cv.IngressAttributes) + require.NotNil(t, cv.IngressAttributes) assert.NotZero(t, cv.IngressAttributes.CommitURL) assert.NotZero(t, cv.IngressAttributes.CommitSHA) }) @@ -201,22 +206,7 @@ func TestConfigurationVersionsUpload(t *testing.T) { ) require.NoError(t, err) - // We do this is a small loop, because it can take a second - // before the upload is finished. - for i := 0; ; i++ { - refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID) - require.NoError(t, err) - - if refreshed.Status == ConfigurationUploaded { - break - } - - if i > 10 { - t.Fatal("Timeout waiting for the configuration version to be uploaded") - } - - time.Sleep(1 * time.Second) - } + WaitUntilStatus(t, client, cv, ConfigurationUploaded, 60) }) t.Run("without a valid upload URL", func(t *testing.T) { @@ -242,6 +232,44 @@ func TestConfigurationVersionsArchive(t *testing.T) { client := testClient(t) ctx := context.Background() + w, wCleanup := createWorkspace(t, client, nil) + defer wCleanup() + + cv, cvCleanup := createConfigurationVersion(t, client, w) + defer cvCleanup() + + t.Run("when the configuration version exists and has been uploaded", func(t *testing.T) { + err := client.ConfigurationVersions.Upload( + ctx, + cv.UploadURL, + "test-fixtures/config-version", + ) + require.NoError(t, err) + + WaitUntilStatus(t, client, cv, ConfigurationUploaded, 60) + + // configuration version should not be archived, since it's the latest version + err = client.ConfigurationVersions.Archive(ctx, cv.ID) + assert.Error(t, err) + assert.EqualError(t, err, "transition not allowed") + + // create subsequent version, since the latest configuration version cannot be archived + newCv, newCvCleanup := createConfigurationVersion(t, client, w) + err = client.ConfigurationVersions.Upload( + ctx, + newCv.UploadURL, + "test-fixtures/config-version", + ) + require.NoError(t, err) + defer newCvCleanup() + WaitUntilStatus(t, client, newCv, ConfigurationUploaded, 60) + + err = client.ConfigurationVersions.Archive(ctx, cv.ID) + require.NoError(t, err) + + WaitUntilStatus(t, client, cv, ConfigurationArchived, 60) + }) + t.Run("when the configuration version does not exist", func(t *testing.T) { err := client.ConfigurationVersions.Archive(ctx, "nonexisting") assert.Equal(t, err, ErrResourceNotFound) @@ -270,7 +298,7 @@ func TestConfigurationVersionsDownload(t *testing.T) { cvFile, err := client.ConfigurationVersions.Download(ctx, uploadedCv.ID) assert.NotNil(t, cvFile) - assert.NoError(t, err) + require.NoError(t, err) assert.True(t, bytes.Equal(cvFile, expectedCvFile.Bytes()), "Configuration version should match") }) diff --git a/cost_estimate.go b/cost_estimate.go index 9dca920df..163f340e5 100644 --- a/cost_estimate.go +++ b/cost_estimate.go @@ -73,13 +73,13 @@ func (s *costEstimates) Read(ctx context.Context, costEstimateID string) (*CostE } u := fmt.Sprintf("cost-estimates/%s", url.QueryEscape(costEstimateID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } ce := &CostEstimate{} - err = s.client.do(ctx, req, ce) + err = req.Do(ctx, ce) if err != nil { return nil, err } @@ -114,13 +114,13 @@ func (s *costEstimates) Logs(ctx context.Context, costEstimateID string) (io.Rea } u := fmt.Sprintf("cost-estimates/%s/output", url.QueryEscape(costEstimateID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } logs := bytes.NewBuffer(nil) - err = s.client.do(ctx, req, logs) + err = req.Do(ctx, logs) if err != nil { return nil, err } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 23eb340d9..cf17bdf2b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -10,26 +10,27 @@ If you are making relevant changes that is worth communicating to our users, ple CHANGELOG.md should have the next minor version listed as `# v1.X.0 (Unreleased)` and any changes can go under there. But if you feel that your changes are better suited for a patch version (like a critical bug fix), you may list a new section for this version. You should repeat the same formatting style introduced by previous versions. -### Scoping pull requests that add new resources +### Scoping pull requests that add new resources -There are instances where several new resources being added (i.e Workspace Run Tasks and Organization Run Tasks) are coalesced into one PR. In order to keep the review process as efficient and least error prone as possible, we ask that you please scope each PR to an individual resource even if the multiple resources you're adding share similarities. If joining multiple related PRs into one single PR makes more sense logistically, we'd ask that you organize your commit history by resource. A general convention for this repository is one commit for the implementation of the resource's methods, one for the integration test, and one for cleanup and housekeeping (e.g modifying the changelog/docs, generating mocks, etc). +There are instances where several new resources being added (i.e Workspace Run Tasks and Organization Run Tasks) are coalesced into one PR. In order to keep the review process as efficient and least error prone as possible, we ask that you please scope each PR to an individual resource even if the multiple resources you're adding share similarities. If joining multiple related PRs into one single PR makes more sense logistically, we'd ask that you organize your commit history by resource. A general convention for this repository is one commit for the implementation of the resource's methods, one for the integration test, and one for cleanup and housekeeping (e.g modifying the changelog/docs, generating mocks, etc). -**Note HashiCorp Employees Only:** When submitting a new set of endpoints please ensure that one of your respective team members approves the changes as well before merging. +**Note HashiCorp Employees Only:** When submitting a new set of endpoints please ensure that one of your respective team members approves the changes as well before merging. ## Running the Linters Locally -1. Ensure you have have [installed golangci-lint](https://golangci-lint.run/usage/install/#local-installation) +1. Ensure you have [installed golangci-lint](https://golangci-lint.run/usage/install/#local-installation) 2. From the CLI, run `golangci-lint run` ## Writing Tests -The test suite contains many acceptance tests that are run against the latest version of Terraform Enterprise. You can read more about running the tests against your own Terraform Enterprise environment in [TESTS.md](TESTS.md). Our CI system (Circle) will not test your fork unless you are an authorized employee, so a HashiCorp maintainer will initiate the tests or you and report any missing tests or simple problems. In order to speed up this process, it's not uncommon for your commits to be incorporated into another PR that we can commit test changes to. +The test suite contains many acceptance tests that are run against the latest version of Terraform Enterprise. You can read more about running the tests against your own Terraform Enterprise environment in [TESTS.md](TESTS.md). Our CI system (Circle) will not test your fork unless you are an authorized employee, so a HashiCorp maintainer will initiate the tests for you and report any missing tests or simple problems. In order to speed up this process, it's not uncommon for your commits to be incorporated into another PR that we can commit test changes to. ## Editor Settings We've included VSCode settings to assist with configuring the go extension. For other editors that integrate with the [Go Language Server](https://github.com/golang/tools/tree/master/gopls), the main thing to do is to add the `integration` build tags so that the test files are found by the language server. See `.vscode/settings.json` for more details. ## Generating Mocks +Ensure you have installed the [mockgen](https://github.com/golang/mock) tool. You'll need to generate mocks if an existing endpoint method is modified or a new method is added. To generate mocks, simply run `./generate_mocks.sh` If you're adding a new API resource to go-tfe, you'll need to add the command to `generate_mocks.sh`. For example if someone creates `example_resource.go`, you'll add: @@ -198,13 +199,13 @@ func (s *example) Create(ctx context.Context, organization string, options Examp } u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } r := &Example{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -222,13 +223,13 @@ func (s *example) List(ctx context.Context, organization string, options *Exampl } u := fmt.Sprintf("organizations/%s/examples", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } el := &ExampleList{} - err = s.client.do(ctx, req, el) + err = req.Do(ctx, el) if err != nil { return nil, err } @@ -251,13 +252,13 @@ func (s *example) ReadWithOptions(ctx context.Context, exampleID string, options } u := fmt.Sprintf("examples/%s", url.QueryEscape(exampleID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } e := &Example{} - err = s.client.do(ctx, req, e) + err = req.Do(ctx, e) if err != nil { return nil, err } @@ -276,13 +277,13 @@ func (s *example) Update(ctx context.Context, exampleID string, options ExampleU } u := fmt.Sprintf("examples/%s", url.QueryEscape(exampleID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } r := &Example{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -297,12 +298,12 @@ func (s *example) Delete(ctx context.Context, exampleID string) error { } u := fmt.Sprintf("examples/%s", exampleID) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *ExampleUpdateOptions) valid() error { diff --git a/docs/TESTS.md b/docs/TESTS.md index b1205cc87..57df55c20 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -12,7 +12,7 @@ Your registry module repository will need to be a [valid module](https://www.ter It will need the following: 1. To be named `terraform--` 1. At least one valid SemVer tag in the format `x.y.z` -[terraform-random-module](ttps://github.com/caseylang/terraform-random-module) is a good example repo. +[terraform-random-module](https://github.com/caseylang/terraform-random-module) is a good example repo. ## 2. Set up environment variables (ENVVARS) diff --git a/errors.go b/errors.go index a04a4d17e..844fae103 100644 --- a/errors.go +++ b/errors.go @@ -26,9 +26,17 @@ var ( ErrUnsupportedPrivateKey = errors.New("private Key can only be present with Azure DevOps Server service provider") + ErrUnsupportedBothTagsRegexAndFileTriggersEnabled = errors.New(`"TagsRegex" cannot be populated when "FileTriggersEnabled" is true`) + + ErrUnsupportedBothTagsRegexAndTriggerPatterns = errors.New(`"TagsRegex" and "TriggerPrefixes" cannot be populated at the same time`) + + ErrUnsupportedBothTagsRegexAndTriggerPrefixes = errors.New(`"TagsRegex" and "TriggerPatterns" cannot be populated at the same time`) + ErrUnsupportedRunTriggerType = errors.New(`"RunTriggerType" must be "inbound" when requesting "include" query params`) ErrUnsupportedBothTriggerPatternsAndPrefixes = errors.New(`"TriggerPatterns" and "TriggerPrefixes" cannot be populated at the same time`) + + ErrUnsupportedBothNamespaceAndPrivateRegistryName = errors.New(`"Namespace" cannot be populated when "RegistryName" is "private"`) ) // Library errors that usually indicate a bug in the implementation of go-tfe @@ -168,7 +176,9 @@ var ( ErrInvalidArch = errors.New("invalid value for arch") - ErrInvalidRegistryName = errors.New("invalid value for registry-name") + ErrInvalidAgentID = errors.New("invalid value for Agent ID") + + ErrInvalidRegistryName = errors.New(`invalid value for registry-name. It must be either "private" or "public"`) ) // Missing values for required field/option @@ -294,4 +304,6 @@ var ( ErrRequiredFilename = errors.New("filename is required") ErrInvalidAsciiArmor = errors.New("ascii armor is invalid") + + ErrRequiredNamespace = errors.New("namespace is required for public registry") ) diff --git a/generate_mocks.sh b/generate_mocks.sh index 924ea62ce..e90e98fc2 100755 --- a/generate_mocks.sh +++ b/generate_mocks.sh @@ -57,3 +57,4 @@ mockgen -source=variable.go -destination=mocks/variable_mocks.go -package=mocks mockgen -source=variable_set.go -destination=mocks/variable_set_mocks.go -package=mocks mockgen -source=workspace.go -destination=mocks/workspace_mocks.go -package=mocks mockgen -source=workspace_run_task.go -destination=mocks/workspace_run_tasks.go -package=mocks +mockgen -source=agent.go -destination=mocks/agents.go -package=mocks diff --git a/gpg_key.go b/gpg_key.go index a56b11156..656074aee 100644 --- a/gpg_key.go +++ b/gpg_key.go @@ -76,13 +76,13 @@ func (s *gpgKeys) Create(ctx context.Context, registryName RegistryName, options } u := fmt.Sprintf("/api/registry/%s/v2/gpg-keys", url.QueryEscape(string(registryName))) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } g := &GPGKey{} - err = s.client.do(ctx, req, g) + err = req.Do(ctx, g) if err != nil { return nil, err } @@ -100,13 +100,13 @@ func (s *gpgKeys) Read(ctx context.Context, keyID GPGKeyID) (*GPGKey, error) { url.QueryEscape(keyID.Namespace), url.QueryEscape(keyID.KeyID), ) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } g := &GPGKey{} - err = s.client.do(ctx, req, g) + err = req.Do(ctx, g) if err != nil { return nil, err } @@ -128,13 +128,13 @@ func (s *gpgKeys) Update(ctx context.Context, keyID GPGKeyID, options GPGKeyUpda url.QueryEscape(keyID.Namespace), url.QueryEscape(keyID.KeyID), ) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } g := &GPGKey{} - err = s.client.do(ctx, req, g) + err = req.Do(ctx, g) if err != nil { if strings.Contains(err.Error(), "namespace not authorized") { return nil, ErrNamespaceNotAuthorized @@ -155,12 +155,12 @@ func (s *gpgKeys) Delete(ctx context.Context, keyID GPGKeyID) error { url.QueryEscape(keyID.Namespace), url.QueryEscape(keyID.KeyID), ) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o GPGKeyID) valid() error { diff --git a/helper_test.go b/helper_test.go index 227796696..84bfab80a 100644 --- a/helper_test.go +++ b/helper_test.go @@ -9,14 +9,18 @@ import ( "encoding/hex" "errors" "fmt" + "io" "io/ioutil" "math/rand" "net/url" "os" + "os/exec" "sync" "testing" "time" + "github.com/stretchr/testify/require" + uuid "github.com/hashicorp/go-uuid" ) @@ -52,8 +56,9 @@ type updateFeatureSetOptions struct { } func testClient(t *testing.T) *Client { - client, err := NewClient(nil) - client.RetryServerErrors(true) // because occasionally we get a 500 internal when deleting an organization's workspace + client, err := NewClient(&Config{ + RetryServerErrors: true, + }) if err != nil { t.Fatal(err) } @@ -84,6 +89,95 @@ func fetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails { return _testAccountDetails } +func createAgent(t *testing.T, client *Client, org *Organization, agentPool *AgentPool, agentPoolToken *AgentToken) (*Agent, func()) { + var orgCleanup func() + var agentPoolCleanup func() + var agentPoolTokenCleanup func() + var agent *Agent + // var pool *AgentPool + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + upgradeOrganizationSubscription(t, client, org) + + if agentPool == nil { + agentPool, agentPoolCleanup = createAgentPool(t, client, org) + t.Log("create, log agentPool: ", agentPool) + } + + if agentPoolToken == nil { + agentPoolToken, agentPoolTokenCleanup = createAgentToken(t, client, agentPool) + } + + // s := "abcdefghijklmnopqrstuvwxyz0123456789" + // containerName := base64.StdEncoding.EncodeToString([]byte(s)) + // t.Log("containerName: ", containerName) + + // containerName := "container-name-test" + ctx := context.Background() + cmd := exec.Command("docker", + "run", "-d", + "--env", "TFC_AGENT_TOKEN="+agentPoolToken.Token, + "--env", "TFC_AGENT_NAME="+"this-is-a-test-agent", + "--env", "TFC_ADDRESS="+"https://tfcdev-2c13224a.ngrok.io", + "docker.mirror.hashicorp.services/hashicorp/tfc-agent:latest") + + go func() { + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Could not run container: %s", err) + } + // t.Log("cmd", cmd) + t.Log("Logging container output: ", (string)(output)) + t.Log("log agent inside container output: ", agent) + }() + + defer func() { + t.Log("Cleaning up agent docker container: ") + cmd := exec.Command("docker", "rm", "-f") + _ = cmd.Run() + }() + + i, err := retry(func() (interface{}, error) { + + agentList, err := client.Agents.List(ctx, agentPool.ID, nil) + if err != nil { + return nil, err + } + + if agentList != nil && len(agentList.Items) > 0 { + return agentList.Items[0], nil + } + return nil, errors.New("No agent found.") + }) + + if err != nil { + t.Fatalf("Could not return an agent %s", err) + } + + agent = i.(*Agent) + t.Log("log agent, after type assertion: ", agent) + + return agent, func() { + if agentPoolTokenCleanup != nil { + // agentPoolTokenCleanup() + t.Log("agentPoolTooken not nil inside helper fn") + } + + if agentPoolCleanup != nil { + // agentPoolCleanup() + t.Log("agentPool not nil") + } + + if orgCleanup != nil { + // orgCleanup() + t.Log("org not nil") + } + } +} + func createAgentPool(t *testing.T, client *Client, org *Organization) (*AgentPool, func()) { var orgCleanup func() @@ -174,26 +268,29 @@ func createUploadedConfigurationVersion(t *testing.T, client *Client, w *Workspa t.Fatal(err) } + WaitUntilStatus(t, client, cv, ConfigurationUploaded, 15) + + return cv, cvCleanup +} + +// helper to wait until a configuration version has reached a certain status +func WaitUntilStatus(t *testing.T, client *Client, cv *ConfigurationVersion, desiredStatus ConfigurationStatus, timeoutSeconds int) { + ctx := context.Background() + for i := 0; ; i++ { - cv, err = client.ConfigurationVersions.Read(ctx, cv.ID) - if err != nil { - cvCleanup() - t.Fatal(err) - } + refreshed, err := client.ConfigurationVersions.Read(ctx, cv.ID) + require.NoError(t, err) - if cv.Status == ConfigurationUploaded { + if refreshed.Status == desiredStatus { break } - if i > 10 { - cvCleanup() - t.Fatal("Timeout waiting for the configuration version to be uploaded") + if i > timeoutSeconds { + t.Fatal("Timeout waiting for the configuration version to be archived") } time.Sleep(1 * time.Second) } - - return cv, cvCleanup } func createGPGKey(t *testing.T, client *Client, org *Organization, provider *RegistryProvider) (*GPGKey, func()) { @@ -595,96 +692,232 @@ func createRunTrigger(t *testing.T, client *Client, w, sourceable *Workspace) (* } } -func createRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { - var wCleanup func() +func createPolicyCheckedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { + return createRunWaitForAnyStatuses(t, client, w, []RunStatus{RunPolicyChecked, RunPolicyOverride}) +} - if w == nil { - w, wCleanup = createWorkspace(t, client, nil) +func createPlannedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { + if paidFeaturesDisabled() { + return createRunWaitForStatus(t, client, w, RunPlanned) + } else { + return createRunWaitForStatus(t, client, w, RunCostEstimated) } +} - cv, cvCleanup := createUploadedConfigurationVersion(t, client, w) +func createCostEstimatedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { + return createRunWaitForStatus(t, client, w, RunCostEstimated) +} +func createRunApply(t *testing.T, client *Client, w *Workspace) (*Run, func()) { ctx := context.Background() - r, err := client.Runs.Create(ctx, RunCreateOptions{ - ConfigurationVersion: cv, - Workspace: w, - }) - if err != nil { - t.Fatal(err) - } + run, rCleanup := createRunUnapplied(t, client, w) + timeout := 2 * time.Minute - return r, func() { - cvCleanup() + // If the run was not in error, it must be applyable + applyRun(t, client, ctx, run) - if wCleanup != nil { - wCleanup() - } + ctxPollRunApplied, cancelPollApplied := context.WithTimeout(ctx, timeout) + + run = pollRunStatus(t, client, ctxPollRunApplied, run, []RunStatus{RunApplied, RunErrored}) + if run.Status == RunErrored { + fatalDumpRunLog(t, client, ctx, run) + } + + return run, func() { + rCleanup() + cancelPollApplied() } } -func createRunWithStatus(t *testing.T, client *Client, w *Workspace, timeout int, desiredStatuses ...RunStatus) (*Run, func()) { +func createRunUnapplied(t *testing.T, client *Client, w *Workspace) (*Run, func()) { + var rCleanup func() + ctx := context.Background() r, rCleanup := createRun(t, client, w) - var err error + timeout := 2 * time.Minute + + ctxPollRunReady, cancelPollRunReady := context.WithTimeout(ctx, timeout) + + run := pollRunStatus( + t, + client, + ctxPollRunReady, + r, + append(applyableStatuses(r), RunErrored), + ) + + if run.Status == RunErrored { + fatalDumpRunLog(t, client, ctx, run) + } + + return run, func() { + rCleanup() + cancelPollRunReady() + } +} + +func createRunWaitForStatus(t *testing.T, client *Client, w *Workspace, status RunStatus) (*Run, func()) { + return createRunWaitForAnyStatuses(t, client, w, []RunStatus{status}) +} + +func createRunWaitForAnyStatuses(t *testing.T, client *Client, w *Workspace, statuses []RunStatus) (*Run, func()) { + var rCleanup func() ctx := context.Background() - for i := 0; ; i++ { - r, err = client.Runs.Read(ctx, r.ID) - if err != nil { - t.Fatal(err) + r, rCleanup := createRun(t, client, w) + + timeout := 2 * time.Minute + + ctxPollRunReady, cancelPollRunReady := context.WithTimeout(ctx, timeout) + + run := pollRunStatus( + t, + client, + ctxPollRunReady, + r, + append(statuses, RunErrored), + ) + + if run.Status == RunErrored { + fatalDumpRunLog(t, client, ctx, run) + } + + return run, func() { + rCleanup() + cancelPollRunReady() + } +} + +func applyableStatuses(r *Run) []RunStatus { + if len(r.PolicyChecks) > 0 { + return []RunStatus{ + RunPolicyChecked, + RunPolicyOverride, } + } else if r.CostEstimate != nil { + return []RunStatus{RunCostEstimated} + } else { + return []RunStatus{RunPlanned} + } +} + +// pollRunStatus will poll the given run until its status matches one of the given run statuses or the given context +// times out. +func pollRunStatus(t *testing.T, client *Client, ctx context.Context, r *Run, rss []RunStatus) *Run { + deadline, ok := ctx.Deadline() + if !ok { + t.Logf("No deadline was set to poll run %q which could result in an infinite loop", r.ID) + } - for _, desiredStatus := range desiredStatuses { - // if we're creating an applied run, we need to manually confirm the apply once the plan finishes - isApplyable := hasApplyableStatus(r) - if desiredStatus == RunApplied && isApplyable { - err := client.Runs.Apply(ctx, r.ID, RunApplyOptions{}) - if err != nil { - t.Fatal(err) + t.Logf("Polling run %q for status included in %q with deadline of %s", r.ID, rss, deadline) + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for finished := false; !finished; { + t.Log("...") + select { + case <-ctx.Done(): + t.Fatalf("Run %q had status %q at deadline", r.ID, r.Status) + case <-ticker.C: + r = readRun(t, client, ctx, r) + t.Logf("Run %q had status %q", r.ID, r.Status) + for _, rs := range rss { + if rs == r.Status { + finished = true + break } - } else if desiredStatus == r.Status { - return r, rCleanup } } + } - if i > timeout { - runStatus := r.Status - rCleanup() - t.Fatal(fmt.Printf("Timeout waiting for run ID %s to reach status %v, had status %s", - r.ID, - desiredStatuses, - runStatus)) - } + return r +} - time.Sleep(1 * time.Second) +// readRun will re-read the given run. +func readRun(t *testing.T, client *Client, ctx context.Context, r *Run) *Run { + t.Logf("Reading run %q", r.ID) + + rr, err := client.Runs.Read(ctx, r.ID) + if err != nil { + t.Fatalf("Could not read run %q: %s", r.ID, err) } + + return rr } -func createPlannedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { - if paidFeaturesDisabled() { - return createRunWithStatus(t, client, w, 45, RunPlanned) +// applyRun will apply the given run. +func applyRun(t *testing.T, client *Client, ctx context.Context, r *Run) { + t.Logf("Applying run %q", r.ID) + + if err := client.Runs.Apply(ctx, r.ID, RunApplyOptions{}); err != nil { + t.Fatalf("Could not apply run %q: %s", r.ID, err) } - return createRunWithStatus(t, client, w, 45, RunCostEstimated) } -func createCostEstimatedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { - return createRunWithStatus(t, client, w, 45, RunCostEstimated) +// readPlan will read the given plan. +func readPlan(t *testing.T, client *Client, ctx context.Context, p *Plan) *Plan { + t.Logf("Reading plan %q", p.ID) + + rp, err := client.Plans.Read(ctx, p.ID) + if err != nil { + t.Fatalf("Could not read plan %q: %s", p.ID, err) + } + + return rp } -func createPolicyCheckedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { - return createRunWithStatus(t, client, w, 45, RunPolicyChecked, RunPolicyOverride) +// readPlanLogs will read the logs of the given plan. +func readPlanLogs(t *testing.T, client *Client, ctx context.Context, p *Plan) io.Reader { + t.Logf("Reading logs of plan %q", p.ID) + + r, err := client.Plans.Logs(ctx, p.ID) + if err != nil { + t.Fatalf("Could not retrieve logs of plan %q: %s", p.ID, err) + } + + return r } -func createAppliedRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { - return createRunWithStatus(t, client, w, 90, RunApplied) +func fatalDumpRunLog(t *testing.T, client *Client, ctx context.Context, run *Run) { + t.Helper() + p := readPlan(t, client, ctx, run.Plan) + r := readPlanLogs(t, client, ctx, p) + + l, err := io.ReadAll(r) + if err != nil { + t.Fatalf("Could not read logs of plan %q: %v", p.ID, err) + } + + t.Log("Run errored - here's some logs to help figure out what happened") + t.Logf("---Start of logs---\n%s\n---End of logs---", l) + + t.Fatalf("Run %q unexpectedly errored", run.ID) } -func hasApplyableStatus(r *Run) bool { - if len(r.PolicyChecks) > 0 { - return r.Status == RunPolicyChecked || r.Status == RunPolicyOverride - } else if r.CostEstimate != nil { - return r.Status == RunCostEstimated - } else { - return r.Status == RunPlanned +func createRun(t *testing.T, client *Client, w *Workspace) (*Run, func()) { + var wCleanup func() + + if w == nil { + w, wCleanup = createWorkspace(t, client, nil) + } + + cv, cvCleanup := createUploadedConfigurationVersion(t, client, w) + + ctx := context.Background() + r, err := client.Runs.Create(ctx, RunCreateOptions{ + ConfigurationVersion: cv, + Workspace: w, + }) + if err != nil { + t.Fatal(err) + } + + return r, func() { + cvCleanup() + + if wCleanup != nil { + wCleanup() + } } } @@ -692,7 +925,7 @@ func createPlanExport(t *testing.T, client *Client, r *Run) (*PlanExport, func() var rCleanup func() if r == nil { - r, rCleanup = createPlannedRun(t, client, nil) + r, rCleanup = createRunApply(t, client, nil) } ctx := context.Background() @@ -704,30 +937,38 @@ func createPlanExport(t *testing.T, client *Client, r *Run) (*PlanExport, func() t.Fatal(err) } - for i := 0; ; i++ { - pe, err := client.PlanExports.Read(ctx, pe.ID) - if err != nil { - t.Fatal(err) - } + timeout := 2 * time.Minute - if pe.Status == PlanExportFinished { - return pe, func() { - if rCleanup != nil { - rCleanup() - } - } - } + ctxPollExportReady, cancelPollExportReady := context.WithTimeout(ctx, timeout) + t.Cleanup(cancelPollExportReady) + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() - if i > 45 { + for { + t.Log("...") + select { + case <-ctxPollExportReady.Done(): rCleanup() - t.Fatal("Timeout waiting for plan export to finish") - } + t.Fatalf("Run %q had status %q at deadline", r.ID, r.Status) + case <-ticker.C: + pe, err := client.PlanExports.Read(ctxPollExportReady, pe.ID) + if err != nil { + t.Fatal(err) + } - time.Sleep(1 * time.Second) + if pe.Status == PlanExportFinished { + return pe, func() { + if rCleanup != nil { + rCleanup() + } + } + } + } } } -func createRegistryModule(t *testing.T, client *Client, org *Organization) (*RegistryModule, func()) { +func createRegistryModule(t *testing.T, client *Client, org *Organization, registryName RegistryName) (*RegistryModule, func()) { var orgCleanup func() if org == nil { @@ -737,9 +978,15 @@ func createRegistryModule(t *testing.T, client *Client, org *Organization) (*Reg ctx := context.Background() options := RegistryModuleCreateOptions{ - Name: String(randomString(t)), - Provider: String("provider"), + Name: String(randomString(t)), + Provider: String("provider"), + RegistryName: registryName, + } + + if registryName == PublicRegistry { + options.Namespace = "namespace" } + rm, err := client.RegistryModules.Create(ctx, org.Name, options) if err != nil { t.Fatal(err) @@ -820,10 +1067,12 @@ func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, f } ctx := context.Background() + description := randomString(t) r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{ - Name: "tst-" + randomString(t), - URL: runTaskURL, - Category: "task", + Name: "tst-" + randomString(t), + URL: runTaskURL, + Description: &description, + Category: "task", }) if err != nil { t.Fatal(err) @@ -1267,12 +1516,12 @@ func createWorkspaceWithVCS(t *testing.T, client *Client, org *Organization, opt } if options.VCSRepo == nil { - options.VCSRepo = &VCSRepoOptions{ - Identifier: String(githubIdentifier), - OAuthTokenID: String(oc.ID), - } + options.VCSRepo = &VCSRepoOptions{} } + options.VCSRepo.Identifier = String(githubIdentifier) + options.VCSRepo.OAuthTokenID = String(oc.ID) + ctx := context.Background() w, err := client.Workspaces.Create(ctx, org.Name, options) if err != nil { @@ -1432,7 +1681,7 @@ func upgradeOrganizationSubscription(t *testing.T, client *Client, organization t.Skip("Can not upgrade an organization's subscription when enterprise is enabled. Set ENABLE_TFE=0 to run.") } - req, err := client.newRequest("GET", "admin/feature-sets", featureSetListOptions{ + req, err := client.NewRequest("GET", "admin/feature-sets", featureSetListOptions{ Q: "Business", }) if err != nil { @@ -1441,7 +1690,7 @@ func upgradeOrganizationSubscription(t *testing.T, client *Client, organization } fsl := &featureSetList{} - err = client.do(context.Background(), req, fsl) + err = req.Do(context.Background(), fsl) if err != nil { t.Fatalf("failed to enumerate feature sets: %v", err) return @@ -1459,13 +1708,13 @@ func upgradeOrganizationSubscription(t *testing.T, client *Client, organization } u := fmt.Sprintf("admin/organizations/%s/subscription", url.QueryEscape(organization.Name)) - req, err = client.newRequest("POST", u, &opts) + req, err = client.NewRequest("POST", u, &opts) if err != nil { t.Fatalf("Failed to create request: %v", err) return } - err = client.do(context.Background(), req, nil) + err = req.Do(context.Background(), nil) if err != nil { t.Fatalf("Failed to upgrade subscription: %v", err) } diff --git a/ip_ranges.go b/ip_ranges.go index c03e3fd1f..07fc61956 100644 --- a/ip_ranges.go +++ b/ip_ranges.go @@ -37,7 +37,7 @@ type IPRange struct { // Read an IPRange that was not modified since the specified date. func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, error) { - req, err := i.client.newRequest("GET", "/api/meta/ip-ranges", nil) + req, err := i.client.NewRequest("GET", "/api/meta/ip-ranges", nil) if err != nil { return nil, err } @@ -47,7 +47,7 @@ func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, er } ir := &IPRange{} - err = i.customDo(ctx, req, ir) + err = req.doIpRanges(ctx, ir) if err != nil { return nil, err } diff --git a/notification_configuration.go b/notification_configuration.go index 97f8da196..0805ee147 100644 --- a/notification_configuration.go +++ b/notification_configuration.go @@ -183,13 +183,13 @@ func (s *notificationConfigurations) List(ctx context.Context, workspaceID strin } u := fmt.Sprintf("workspaces/%s/notification-configurations", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } ncl := &NotificationConfigurationList{} - err = s.client.do(ctx, req, ncl) + err = req.Do(ctx, ncl) if err != nil { return nil, err } @@ -207,13 +207,13 @@ func (s *notificationConfigurations) Create(ctx context.Context, workspaceID str } u := fmt.Sprintf("workspaces/%s/notification-configurations", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } nc := &NotificationConfiguration{} - err = s.client.do(ctx, req, nc) + err = req.Do(ctx, nc) if err != nil { return nil, err } @@ -228,13 +228,13 @@ func (s *notificationConfigurations) Read(ctx context.Context, notificationConfi } u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } nc := &NotificationConfiguration{} - err = s.client.do(ctx, req, nc) + err = req.Do(ctx, nc) if err != nil { return nil, err } @@ -253,13 +253,13 @@ func (s *notificationConfigurations) Update(ctx context.Context, notificationCon } u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } nc := &NotificationConfiguration{} - err = s.client.do(ctx, req, nc) + err = req.Do(ctx, nc) if err != nil { return nil, err } @@ -274,12 +274,12 @@ func (s *notificationConfigurations) Delete(ctx context.Context, notificationCon } u := fmt.Sprintf("notification-configurations/%s", url.QueryEscape(notificationConfigurationID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Verify a notification configuration by delivering a verification @@ -291,13 +291,13 @@ func (s *notificationConfigurations) Verify(ctx context.Context, notificationCon u := fmt.Sprintf( "notification-configurations/%s/actions/verify", url.QueryEscape(notificationConfigurationID)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } nc := &NotificationConfiguration{} - err = s.client.do(ctx, req, nc) + err = req.Do(ctx, nc) if err != nil { return nil, err } diff --git a/oauth_client.go b/oauth_client.go index 4be2321af..e57d49afb 100644 --- a/oauth_client.go +++ b/oauth_client.go @@ -73,6 +73,7 @@ type OAuthClient struct { HTTPURL string `jsonapi:"attr,http-url"` Key string `jsonapi:"attr,key"` RSAPublicKey string `jsonapi:"attr,rsa-public-key"` + Name *string `jsonapi:"attr,name"` Secret string `jsonapi:"attr,secret"` ServiceProvider ServiceProviderType `jsonapi:"attr,service-provider"` ServiceProviderName string `jsonapi:"attr,service-provider-display-name"` @@ -167,13 +168,13 @@ func (s *oAuthClients) List(ctx context.Context, organization string, options *O } u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } ocl := &OAuthClientList{} - err = s.client.do(ctx, req, ocl) + err = req.Do(ctx, ocl) if err != nil { return nil, err } @@ -191,13 +192,13 @@ func (s *oAuthClients) Create(ctx context.Context, organization string, options } u := fmt.Sprintf("organizations/%s/oauth-clients", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } oc := &OAuthClient{} - err = s.client.do(ctx, req, oc) + err = req.Do(ctx, oc) if err != nil { return nil, err } @@ -212,13 +213,13 @@ func (s *oAuthClients) Read(ctx context.Context, oAuthClientID string) (*OAuthCl } u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } oc := &OAuthClient{} - err = s.client.do(ctx, req, oc) + err = req.Do(ctx, oc) if err != nil { return nil, err } @@ -233,13 +234,13 @@ func (s *oAuthClients) Update(ctx context.Context, oAuthClientID string, options } u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } oc := &OAuthClient{} - err = s.client.do(ctx, req, oc) + err = req.Do(ctx, oc) if err != nil { return nil, err } @@ -254,12 +255,12 @@ func (s *oAuthClients) Delete(ctx context.Context, oAuthClientID string) error { } u := fmt.Sprintf("oauth-clients/%s", url.QueryEscape(oAuthClientID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o OAuthClientCreateOptions) valid() error { diff --git a/oauth_client_integration_test.go b/oauth_client_integration_test.go index b9b0817d1..0175b606b 100644 --- a/oauth_client_integration_test.go +++ b/oauth_client_integration_test.go @@ -5,6 +5,7 @@ package tfe import ( "context" + "fmt" "os" "testing" @@ -72,6 +73,9 @@ func TestOAuthClientsList(t *testing.T) { Include: []OAuthClientIncludeOpt{OauthClientOauthTokens}, }) require.NoError(t, err) + require.NotEmpty(t, ocl.Items) + require.NotNil(t, ocl.Items[0]) + require.NotEmpty(t, ocl.Items[0].OAuthTokens) assert.NotEmpty(t, ocl.Items[0].OAuthTokens[0].ID) }) @@ -103,8 +107,9 @@ func TestOAuthClientsCreate(t *testing.T) { } oc, err := client.OAuthClients.Create(ctx, orgTest.Name, options) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, oc.ID) + assert.Nil(t, oc.Name) assert.Equal(t, "https://api.github.com", oc.APIURL) assert.Equal(t, "https://github.com", oc.HTTPURL) assert.Equal(t, 1, len(oc.OAuthTokens)) @@ -191,7 +196,7 @@ func TestOAuthClientsCreate_rsaKeyPair(t *testing.T) { } oc, err := client.OAuthClients.Create(ctx, orgTest.Name, options) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, oc.ID) assert.Equal(t, "https://bbs.com", oc.APIURL) assert.Equal(t, "https://bbs.com", oc.HTTPURL) @@ -247,8 +252,14 @@ func TestOAuthClientsDelete(t *testing.T) { err := client.OAuthClients.Delete(ctx, ocTest.ID) require.NoError(t, err) - // Try loading the OAuth client - it should fail. - _, err = client.OAuthClients.Read(ctx, ocTest.ID) + _, err = retry(func() (interface{}, error) { + c, err := client.OAuthClients.Read(ctx, ocTest.ID) + if err != ErrResourceNotFound { + return nil, fmt.Errorf("expected %s, but err was %s", ErrResourceNotFound, err) + } + return c, err + }) + assert.Equal(t, err, ErrResourceNotFound) }) @@ -403,7 +414,7 @@ func TestOAuthClientsUpdate_rsaKeyPair(t *testing.T) { } origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, origOC.ID) newKey := randomString(t) @@ -411,7 +422,7 @@ func TestOAuthClientsUpdate_rsaKeyPair(t *testing.T) { Key: String(newKey), } oc, err := client.OAuthClients.Update(ctx, origOC.ID, updateOpts) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, oc.ID) assert.Equal(t, ServiceProviderBitbucketServer, oc.ServiceProvider) assert.Equal(t, oc.RSAPublicKey, origOC.RSAPublicKey) @@ -430,7 +441,7 @@ func TestOAuthClientsUpdate_rsaKeyPair(t *testing.T) { } origOC, err := client.OAuthClients.Create(ctx, orgTest.Name, options) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, origOC.ID) updateOpts := OAuthClientUpdateOptions{ diff --git a/oauth_token.go b/oauth_token.go index 2c882498c..ddefc23a7 100644 --- a/oauth_token.go +++ b/oauth_token.go @@ -77,13 +77,13 @@ func (s *oAuthTokens) List(ctx context.Context, organization string, options *OA } u := fmt.Sprintf("organizations/%s/oauth-tokens", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } otl := &OAuthTokenList{} - err = s.client.do(ctx, req, otl) + err = req.Do(ctx, otl) if err != nil { return nil, err } @@ -98,13 +98,13 @@ func (s *oAuthTokens) Read(ctx context.Context, oAuthTokenID string) (*OAuthToke } u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } ot := &OAuthToken{} - err = s.client.do(ctx, req, ot) + err = req.Do(ctx, ot) if err != nil { return nil, err } @@ -119,13 +119,13 @@ func (s *oAuthTokens) Update(ctx context.Context, oAuthTokenID string, options O } u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } ot := &OAuthToken{} - err = s.client.do(ctx, req, ot) + err = req.Do(ctx, ot) if err != nil { return nil, err } @@ -140,10 +140,10 @@ func (s *oAuthTokens) Delete(ctx context.Context, oAuthTokenID string) error { } u := fmt.Sprintf("oauth-tokens/%s", url.QueryEscape(oAuthTokenID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/organization.go b/organization.go index 8b3b3941b..6d1d31b45 100644 --- a/organization.go +++ b/organization.go @@ -200,13 +200,13 @@ type ReadRunQueueOptions struct { // List all the organizations visible to the current user. func (s *organizations) List(ctx context.Context, options *OrganizationListOptions) (*OrganizationList, error) { - req, err := s.client.newRequest("GET", "organizations", options) + req, err := s.client.NewRequest("GET", "organizations", options) if err != nil { return nil, err } orgl := &OrganizationList{} - err = s.client.do(ctx, req, orgl) + err = req.Do(ctx, orgl) if err != nil { return nil, err } @@ -220,13 +220,13 @@ func (s *organizations) Create(ctx context.Context, options OrganizationCreateOp return nil, err } - req, err := s.client.newRequest("POST", "organizations", &options) + req, err := s.client.NewRequest("POST", "organizations", &options) if err != nil { return nil, err } org := &Organization{} - err = s.client.do(ctx, req, org) + err = req.Do(ctx, org) if err != nil { return nil, err } @@ -241,13 +241,13 @@ func (s *organizations) Read(ctx context.Context, organization string) (*Organiz } u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } org := &Organization{} - err = s.client.do(ctx, req, org) + err = req.Do(ctx, org) if err != nil { return nil, err } @@ -262,13 +262,13 @@ func (s *organizations) Update(ctx context.Context, organization string, options } u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } org := &Organization{} - err = s.client.do(ctx, req, org) + err = req.Do(ctx, org) if err != nil { return nil, err } @@ -283,12 +283,12 @@ func (s *organizations) Delete(ctx context.Context, organization string) error { } u := fmt.Sprintf("organizations/%s", url.QueryEscape(organization)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // ReadCapacity shows the currently used capacity of an organization. @@ -298,13 +298,13 @@ func (s *organizations) ReadCapacity(ctx context.Context, organization string) ( } u := fmt.Sprintf("organizations/%s/capacity", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } c := &Capacity{} - err = s.client.do(ctx, req, c) + err = req.Do(ctx, c) if err != nil { return nil, err } @@ -319,13 +319,13 @@ func (s *organizations) ReadEntitlements(ctx context.Context, organization strin } u := fmt.Sprintf("organizations/%s/entitlement-set", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } e := &Entitlements{} - err = s.client.do(ctx, req, e) + err = req.Do(ctx, e) if err != nil { return nil, err } @@ -340,13 +340,13 @@ func (s *organizations) ReadRunQueue(ctx context.Context, organization string, o } u := fmt.Sprintf("organizations/%s/runs/queue", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, &options) + req, err := s.client.NewRequest("GET", u, &options) if err != nil { return nil, err } rq := &RunQueue{} - err = s.client.do(ctx, req, rq) + err = req.Do(ctx, rq) if err != nil { return nil, err } diff --git a/organization_membership.go b/organization_membership.go index aab19eea5..396b3a94a 100644 --- a/organization_membership.go +++ b/organization_membership.go @@ -111,13 +111,13 @@ func (s *organizationMemberships) List(ctx context.Context, organization string, } u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } ml := &OrganizationMembershipList{} - err = s.client.do(ctx, req, ml) + err = req.Do(ctx, ml) if err != nil { return nil, err } @@ -135,13 +135,13 @@ func (s *organizationMemberships) Create(ctx context.Context, organization strin } u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } m := &OrganizationMembership{} - err = s.client.do(ctx, req, m) + err = req.Do(ctx, m) if err != nil { return nil, err } @@ -164,13 +164,13 @@ func (s *organizationMemberships) ReadWithOptions(ctx context.Context, organizat } u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID)) - req, err := s.client.newRequest("GET", u, &options) + req, err := s.client.NewRequest("GET", u, &options) if err != nil { return nil, err } mem := &OrganizationMembership{} - err = s.client.do(ctx, req, mem) + err = req.Do(ctx, mem) if err != nil { return nil, err } @@ -185,12 +185,12 @@ func (s *organizationMemberships) Delete(ctx context.Context, organizationMember } u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o OrganizationMembershipCreateOptions) valid() error { diff --git a/organization_tags.go b/organization_tags.go index bbf756f74..81e688e4a 100644 --- a/organization_tags.go +++ b/organization_tags.go @@ -82,13 +82,13 @@ func (s *organizationTags) List(ctx context.Context, organization string, option } u := fmt.Sprintf("organizations/%s/tags", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } tags := &OrganizationTagsList{} - err = s.client.do(ctx, req, tags) + err = req.Do(ctx, tags) if err != nil { return nil, err } @@ -112,12 +112,12 @@ func (s *organizationTags) Delete(ctx context.Context, organization string, opti tagsToRemove = append(tagsToRemove, &tagID{ID: id}) } - req, err := s.client.newRequest("DELETE", u, tagsToRemove) + req, err := s.client.NewRequest("DELETE", u, tagsToRemove) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Add workspaces to a tag @@ -136,12 +136,12 @@ func (s *organizationTags) AddWorkspaces(ctx context.Context, tag string, option } u := fmt.Sprintf("tags/%s/relationships/workspaces", url.QueryEscape(tag)) - req, err := s.client.newRequest("POST", u, workspaces) + req, err := s.client.NewRequest("POST", u, workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (opts *OrganizationTagsDeleteOptions) valid() error { diff --git a/organization_token.go b/organization_token.go index 263f38cf7..241541dce 100644 --- a/organization_token.go +++ b/organization_token.go @@ -47,13 +47,13 @@ func (s *organizationTokens) Create(ctx context.Context, organization string) (* } u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } ot := &OrganizationToken{} - err = s.client.do(ctx, req, ot) + err = req.Do(ctx, ot) if err != nil { return nil, err } @@ -68,13 +68,13 @@ func (s *organizationTokens) Read(ctx context.Context, organization string) (*Or } u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } ot := &OrganizationToken{} - err = s.client.do(ctx, req, ot) + err = req.Do(ctx, ot) if err != nil { return nil, err } @@ -89,10 +89,10 @@ func (s *organizationTokens) Delete(ctx context.Context, organization string) er } u := fmt.Sprintf("organizations/%s/authentication-token", url.QueryEscape(organization)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/organization_token_integration_test.go b/organization_token_integration_test.go index e60736ffd..4570444f9 100644 --- a/organization_token_integration_test.go +++ b/organization_token_integration_test.go @@ -51,7 +51,7 @@ func TestOrganizationTokensRead(t *testing.T) { _, otTestCleanup := createOrganizationToken(t, client, orgTest) ot, err := client.OrganizationTokens.Read(ctx, orgTest.Name) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, ot) otTestCleanup() @@ -81,7 +81,7 @@ func TestOrganizationTokensDelete(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.OrganizationTokens.Delete(ctx, orgTest.Name) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("when a token does not exist", func(t *testing.T) { diff --git a/plan.go b/plan.go index 67674be87..993f875aa 100644 --- a/plan.go +++ b/plan.go @@ -80,13 +80,13 @@ func (s *plans) Read(ctx context.Context, planID string) (*Plan, error) { } u := fmt.Sprintf("plans/%s", url.QueryEscape(planID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } p := &Plan{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -145,13 +145,13 @@ func (s *plans) ReadJSONOutput(ctx context.Context, planID string) ([]byte, erro } u := fmt.Sprintf("plans/%s/json-output", url.QueryEscape(planID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } var buf bytes.Buffer - err = s.client.do(ctx, req, &buf) + err = req.Do(ctx, &buf) if err != nil { return nil, err } diff --git a/plan_export.go b/plan_export.go index 35dc907b1..8151f619d 100644 --- a/plan_export.go +++ b/plan_export.go @@ -93,13 +93,13 @@ func (s *planExports) Create(ctx context.Context, options PlanExportCreateOption return nil, err } - req, err := s.client.newRequest("POST", "plan-exports", &options) + req, err := s.client.NewRequest("POST", "plan-exports", &options) if err != nil { return nil, err } pe := &PlanExport{} - err = s.client.do(ctx, req, pe) + err = req.Do(ctx, pe) if err != nil { return nil, err } @@ -114,13 +114,13 @@ func (s *planExports) Read(ctx context.Context, planExportID string) (*PlanExpor } u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } pe := &PlanExport{} - err = s.client.do(ctx, req, pe) + err = req.Do(ctx, pe) if err != nil { return nil, err } @@ -135,12 +135,12 @@ func (s *planExports) Delete(ctx context.Context, planExportID string) error { } u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Download a plan export's data. Data is exported in a .tar.gz format. @@ -150,13 +150,13 @@ func (s *planExports) Download(ctx context.Context, planExportID string) ([]byte } u := fmt.Sprintf("plan-exports/%s/download", url.QueryEscape(planExportID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } var buf bytes.Buffer - err = s.client.do(ctx, req, &buf) + err = req.Do(ctx, &buf) if err != nil { return nil, err } diff --git a/plan_export_integration_test.go b/plan_export_integration_test.go index acb2f211b..5802c578f 100644 --- a/plan_export_integration_test.go +++ b/plan_export_integration_test.go @@ -22,7 +22,7 @@ func TestPlanExportsCreate(t *testing.T) { defer rTestCleanup() pTest, err := client.Plans.Read(ctx, rTest.Plan.ID) - assert.NoError(t, err) + require.NoError(t, err) t.Run("with valid options", func(t *testing.T) { options := PlanExportCreateOptions{ @@ -91,7 +91,7 @@ func TestPlanExportsDelete(t *testing.T) { t.Run("with a valid ID", func(t *testing.T) { err := client.PlanExports.Delete(ctx, peTest.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("when the export does not exist", func(t *testing.T) { @@ -115,7 +115,7 @@ func TestPlanExportsDownload(t *testing.T) { t.Run("with a valid ID", func(t *testing.T) { pe, err := client.PlanExports.Download(ctx, peTest.ID) assert.NotNil(t, pe) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("without a valid ID", func(t *testing.T) { diff --git a/policy.go b/policy.go index 49a293a5a..3443cb21a 100644 --- a/policy.go +++ b/policy.go @@ -132,13 +132,13 @@ func (s *policies) List(ctx context.Context, organization string, options *Polic } u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } pl := &PolicyList{} - err = s.client.do(ctx, req, pl) + err = req.Do(ctx, pl) if err != nil { return nil, err } @@ -156,13 +156,13 @@ func (s *policies) Create(ctx context.Context, organization string, options Poli } u := fmt.Sprintf("organizations/%s/policies", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } p := &Policy{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -177,13 +177,13 @@ func (s *policies) Read(ctx context.Context, policyID string) (*Policy, error) { } u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } p := &Policy{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -198,13 +198,13 @@ func (s *policies) Update(ctx context.Context, policyID string, options PolicyUp } u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } p := &Policy{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -219,12 +219,12 @@ func (s *policies) Delete(ctx context.Context, policyID string) error { } u := fmt.Sprintf("policies/%s", url.QueryEscape(policyID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Upload the policy content of the policy. @@ -234,12 +234,12 @@ func (s *policies) Upload(ctx context.Context, policyID string, content []byte) } u := fmt.Sprintf("policies/%s/upload", url.QueryEscape(policyID)) - req, err := s.client.newRequest("PUT", u, content) + req, err := s.client.NewRequest("PUT", u, content) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Download the policy content of the policy. @@ -249,13 +249,13 @@ func (s *policies) Download(ctx context.Context, policyID string) ([]byte, error } u := fmt.Sprintf("policies/%s/download", url.QueryEscape(policyID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } var buf bytes.Buffer - err = s.client.do(ctx, req, &buf) + err = req.Do(ctx, &buf) if err != nil { return nil, err } diff --git a/policy_check.go b/policy_check.go index 0a1dea114..549da79a5 100644 --- a/policy_check.go +++ b/policy_check.go @@ -138,13 +138,13 @@ func (s *policyChecks) List(ctx context.Context, runID string, options *PolicyCh } u := fmt.Sprintf("runs/%s/policy-checks", url.QueryEscape(runID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } pcl := &PolicyCheckList{} - err = s.client.do(ctx, req, pcl) + err = req.Do(ctx, pcl) if err != nil { return nil, err } @@ -159,13 +159,13 @@ func (s *policyChecks) Read(ctx context.Context, policyCheckID string) (*PolicyC } u := fmt.Sprintf("policy-checks/%s", url.QueryEscape(policyCheckID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } pc := &PolicyCheck{} - err = s.client.do(ctx, req, pc) + err = req.Do(ctx, pc) if err != nil { return nil, err } @@ -180,13 +180,13 @@ func (s *policyChecks) Override(ctx context.Context, policyCheckID string) (*Pol } u := fmt.Sprintf("policy-checks/%s/actions/override", url.QueryEscape(policyCheckID)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } pc := &PolicyCheck{} - err = s.client.do(ctx, req, pc) + err = req.Do(ctx, pc) if err != nil { return nil, err } @@ -220,13 +220,13 @@ func (s *policyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reade } u := fmt.Sprintf("policy-checks/%s/output", url.QueryEscape(policyCheckID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } logs := bytes.NewBuffer(nil) - err = s.client.do(ctx, req, logs) + err = req.Do(ctx, logs) if err != nil { return nil, err } diff --git a/policy_check_integration_test.go b/policy_check_integration_test.go index df3941ba5..32d729c7e 100644 --- a/policy_check_integration_test.go +++ b/policy_check_integration_test.go @@ -68,6 +68,9 @@ func TestPolicyChecksList(t *testing.T) { Include: []PolicyCheckIncludeOpt{PolicyCheckRun}, }) require.NoError(t, err) + require.NotEmpty(t, pcl.Items) + require.NotNil(t, pcl.Items[0]) + require.NotNil(t, pcl.Items[0].Run) assert.NotEmpty(t, pcl.Items[0].Run.Status) }) diff --git a/policy_integration_test.go b/policy_integration_test.go index 2e52b4955..ee5ed331d 100644 --- a/policy_integration_test.go +++ b/policy_integration_test.go @@ -372,17 +372,17 @@ func TestPoliciesUpload(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.Policies.Upload(ctx, pTest.ID, []byte(`main = rule { true }`)) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with empty content", func(t *testing.T) { err := client.Policies.Upload(ctx, pTest.ID, []byte{}) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("without any content", func(t *testing.T) { err := client.Policies.Upload(ctx, pTest.ID, nil) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("without a valid policy ID", func(t *testing.T) { @@ -413,7 +413,7 @@ func TestPoliciesDownload(t *testing.T) { require.NoError(t, err) content, err := client.Policies.Download(ctx, pTest.ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, testContent, content) }) diff --git a/policy_set.go b/policy_set.go index ff0e021a3..b2157b0f7 100644 --- a/policy_set.go +++ b/policy_set.go @@ -217,13 +217,13 @@ func (s *policySets) List(ctx context.Context, organization string, options *Pol } u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } psl := &PolicySetList{} - err = s.client.do(ctx, req, psl) + err = req.Do(ctx, psl) if err != nil { return nil, err } @@ -241,13 +241,13 @@ func (s *policySets) Create(ctx context.Context, organization string, options Po } u := fmt.Sprintf("organizations/%s/policy-sets", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } ps := &PolicySet{} - err = s.client.do(ctx, req, ps) + err = req.Do(ctx, ps) if err != nil { return nil, err } @@ -270,13 +270,13 @@ func (s *policySets) ReadWithOptions(ctx context.Context, policySetID string, op } u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } ps := &PolicySet{} - err = s.client.do(ctx, req, ps) + err = req.Do(ctx, ps) if err != nil { return nil, err } @@ -294,13 +294,13 @@ func (s *policySets) Update(ctx context.Context, policySetID string, options Pol } u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } ps := &PolicySet{} - err = s.client.do(ctx, req, ps) + err = req.Do(ctx, ps) if err != nil { return nil, err } @@ -318,12 +318,12 @@ func (s *policySets) AddPolicies(ctx context.Context, policySetID string, option } u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("POST", u, options.Policies) + req, err := s.client.NewRequest("POST", u, options.Policies) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // RemovePolicies remove policies from a policy set @@ -336,12 +336,12 @@ func (s *policySets) RemovePolicies(ctx context.Context, policySetID string, opt } u := fmt.Sprintf("policy-sets/%s/relationships/policies", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("DELETE", u, options.Policies) + req, err := s.client.NewRequest("DELETE", u, options.Policies) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Addworkspaces adds workspaces to a policy set. @@ -354,12 +354,12 @@ func (s *policySets) AddWorkspaces(ctx context.Context, policySetID string, opti } u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("POST", u, options.Workspaces) + req, err := s.client.NewRequest("POST", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // RemoveWorkspaces removes workspaces from a policy set. @@ -372,12 +372,12 @@ func (s *policySets) RemoveWorkspaces(ctx context.Context, policySetID string, o } u := fmt.Sprintf("policy-sets/%s/relationships/workspaces", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("DELETE", u, options.Workspaces) + req, err := s.client.NewRequest("DELETE", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Delete a policy set by its ID. @@ -387,12 +387,12 @@ func (s *policySets) Delete(ctx context.Context, policySetID string) error { } u := fmt.Sprintf("policy-sets/%s", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o PolicySetCreateOptions) valid() error { diff --git a/policy_set_integration_test.go b/policy_set_integration_test.go index 2cd02cd56..f1b7e6761 100644 --- a/policy_set_integration_test.go +++ b/policy_set_integration_test.go @@ -290,11 +290,13 @@ func TestPolicySetsRead(t *testing.T) { // The newest policy set version is changed to the most recent one // that was created. + require.NotNil(t, psWithOptions.NewestVersion) assert.Equal(t, psWithOptions.NewestVersion.ID, psvNew.ID) assert.Equal(t, psWithOptions.NewestVersion.Status, PolicySetVersionPending) // The current one is now set because policies were uploaded to the // policy set version. Notice how it is set to the one that was uplaoded, // not the newest policy set version. + require.NotNil(t, psWithOptions.CurrentVersion) assert.Equal(t, psWithOptions.CurrentVersion.ID, psv.ID) assert.Equal(t, psWithOptions.CurrentVersion.Status, PolicySetVersionReady) }) diff --git a/policy_set_parameter.go b/policy_set_parameter.go index 5d5201756..ca2be27c0 100644 --- a/policy_set_parameter.go +++ b/policy_set_parameter.go @@ -104,13 +104,13 @@ func (s *policySetParameters) List(ctx context.Context, policySetID string, opti } u := fmt.Sprintf("policy-sets/%s/parameters", policySetID) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } vl := &PolicySetParameterList{} - err = s.client.do(ctx, req, vl) + err = req.Do(ctx, vl) if err != nil { return nil, err } @@ -128,13 +128,13 @@ func (s *policySetParameters) Create(ctx context.Context, policySetID string, op } u := fmt.Sprintf("policy-sets/%s/parameters", url.QueryEscape(policySetID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } p := &PolicySetParameter{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -152,13 +152,13 @@ func (s *policySetParameters) Read(ctx context.Context, policySetID, parameterID } u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } p := &PolicySetParameter{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -176,13 +176,13 @@ func (s *policySetParameters) Update(ctx context.Context, policySetID, parameter } u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } p := &PolicySetParameter{} - err = s.client.do(ctx, req, p) + err = req.Do(ctx, p) if err != nil { return nil, err } @@ -200,12 +200,12 @@ func (s *policySetParameters) Delete(ctx context.Context, policySetID, parameter } u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o PolicySetParameterCreateOptions) valid() error { diff --git a/policy_set_parameter_integration_test.go b/policy_set_parameter_integration_test.go index 1ab4c5c50..8975b9912 100644 --- a/policy_set_parameter_integration_test.go +++ b/policy_set_parameter_integration_test.go @@ -283,7 +283,7 @@ func TestPolicySetParametersDelete(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.PolicySetParameters.Delete(ctx, psTest.ID, pTest.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with non existing parameter ID", func(t *testing.T) { diff --git a/policy_set_version.go b/policy_set_version.go index 6d31ae876..b1d075507 100644 --- a/policy_set_version.go +++ b/policy_set_version.go @@ -102,13 +102,13 @@ func (p *policySetVersions) Create(ctx context.Context, policySetID string) (*Po } u := fmt.Sprintf("policy-sets/%s/versions", url.QueryEscape(policySetID)) - req, err := p.client.newRequest("POST", u, nil) + req, err := p.client.NewRequest("POST", u, nil) if err != nil { return nil, err } psv := &PolicySetVersion{} - err = p.client.do(ctx, req, psv) + err = req.Do(ctx, psv) if err != nil { return nil, err } @@ -123,13 +123,13 @@ func (p *policySetVersions) Read(ctx context.Context, policySetVersionID string) } u := fmt.Sprintf("policy-set-versions/%s", url.QueryEscape(policySetVersionID)) - req, err := p.client.newRequest("GET", u, nil) + req, err := p.client.NewRequest("GET", u, nil) if err != nil { return nil, err } psv := &PolicySetVersion{} - err = p.client.do(ctx, req, psv) + err = req.Do(ctx, psv) if err != nil { return nil, err } @@ -151,10 +151,10 @@ func (p *policySetVersions) Upload(ctx context.Context, psv PolicySetVersion, pa return err } - req, err := p.client.newRequest("PUT", uploadURL, body) + req, err := p.client.NewRequest("PUT", uploadURL, body) if err != nil { return err } - return p.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/registry_module.go b/registry_module.go index 1e0605f3d..464ed1810 100644 --- a/registry_module.go +++ b/registry_module.go @@ -3,7 +3,9 @@ package tfe import ( "context" "fmt" + "log" "net/url" + "strings" ) // Compile-time proof of interface implementation. @@ -82,6 +84,11 @@ type RegistryModuleID struct { Name string // The module's provider, see RegistryModule.Provider Provider string + // The namespace of the module. For private modules this is the name of the organization that owns the module + // Required for public modules + Namespace string + // Either public or private. If not provided, defaults to private + RegistryName RegistryName } // RegistryModuleList represents a list of registry modules. @@ -95,6 +102,8 @@ type RegistryModule struct { ID string `jsonapi:"primary,registry-modules"` Name string `jsonapi:"attr,name"` Provider string `jsonapi:"attr,provider"` + RegistryName RegistryName `jsonapi:"attr,registry-name"` + Namespace string `jsonapi:"attr,namespace"` Permissions *RegistryModulePermissions `jsonapi:"attr,permissions"` Status RegistryModuleStatus `jsonapi:"attr,status"` VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"` @@ -150,6 +159,11 @@ type RegistryModuleCreateOptions struct { Name *string `jsonapi:"attr,name"` // Required: Provider *string `jsonapi:"attr,provider"` + // Optional: Whether this is a publicly maintained module or private. Must be either public or private. + // Defaults to private if not specified + RegistryName RegistryName `jsonapi:"attr,registry-name,omitempty"` + // Optional: The namespace of this module. Required for public modules only. + Namespace string `jsonapi:"attr,namespace,omitempty"` } // RegistryModuleCreateVersionOptions is used when creating a registry module version @@ -188,13 +202,13 @@ func (s *registryModules) List(ctx context.Context, organization string, options } u := fmt.Sprintf("organizations/%s/registry-modules", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } ml := &RegistryModuleList{} - err = s.client.do(ctx, req, ml) + err = req.Do(ctx, ml) if err != nil { return nil, err } @@ -216,12 +230,12 @@ func (r *registryModules) Upload(ctx context.Context, rmv RegistryModuleVersion, return err } - req, err := r.client.newRequest("PUT", uploadURL, body) + req, err := r.client.NewRequest("PUT", uploadURL, body) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Create a new registry module without a VCS repo @@ -237,13 +251,13 @@ func (r *registryModules) Create(ctx context.Context, organization string, optio "organizations/%s/registry-modules", url.QueryEscape(organization), ) - req, err := r.client.newRequest("POST", u, &options) + req, err := r.client.NewRequest("POST", u, &options) if err != nil { return nil, err } rm := &RegistryModule{} - err = r.client.do(ctx, req, rm) + err = req.Do(ctx, rm) if err != nil { return nil, err } @@ -267,13 +281,13 @@ func (r *registryModules) CreateVersion(ctx context.Context, moduleID RegistryMo url.QueryEscape(moduleID.Name), url.QueryEscape(moduleID.Provider), ) - req, err := r.client.newRequest("POST", u, &options) + req, err := r.client.NewRequest("POST", u, &options) if err != nil { return nil, err } rmv := &RegistryModuleVersion{} - err = r.client.do(ctx, req, rmv) + err = req.Do(ctx, rmv) if err != nil { return nil, err } @@ -287,13 +301,13 @@ func (r *registryModules) CreateWithVCSConnection(ctx context.Context, options R return nil, err } - req, err := r.client.newRequest("POST", "registry-modules", &options) + req, err := r.client.NewRequest("POST", "registry-modules", &options) if err != nil { return nil, err } rm := &RegistryModule{} - err = r.client.do(ctx, req, rm) + err = req.Do(ctx, rm) if err != nil { return nil, err } @@ -307,19 +321,32 @@ func (r *registryModules) Read(ctx context.Context, moduleID RegistryModuleID) ( return nil, err } + if moduleID.RegistryName == "" { + log.Println("[WARN] Support for using the RegistryModuleID without RegistryName is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the RegistryName in RegistryModuleID.") + moduleID.RegistryName = PrivateRegistry + } + + if moduleID.RegistryName == PrivateRegistry && strings.TrimSpace(moduleID.Namespace) == "" { + log.Println("[WARN] Support for using the RegistryModuleID without Namespace is deprecated as of release 1.5.0 and may be removed in a future version. The preferred method is to include the Namespace in RegistryModuleID.") + moduleID.Namespace = moduleID.Organization + } + u := fmt.Sprintf( - "registry-modules/show/%s/%s/%s", + "organizations/%s/registry-modules/%s/%s/%s/%s", url.QueryEscape(moduleID.Organization), + url.QueryEscape(string(moduleID.RegistryName)), + url.QueryEscape(moduleID.Namespace), url.QueryEscape(moduleID.Name), url.QueryEscape(moduleID.Provider), ) - req, err := r.client.newRequest("GET", u, nil) + + req, err := r.client.NewRequest("GET", u, nil) if err != nil { return nil, err } rm := &RegistryModule{} - err = r.client.do(ctx, req, rm) + err = req.Do(ctx, rm) if err != nil { return nil, err } @@ -344,12 +371,12 @@ func (r *registryModules) Delete(ctx context.Context, organization, name string) url.QueryEscape(organization), url.QueryEscape(name), ) - req, err := r.client.newRequest("POST", u, nil) + req, err := r.client.NewRequest("POST", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // DeleteProvider is used to delete the specific registry module provider @@ -364,12 +391,12 @@ func (r *registryModules) DeleteProvider(ctx context.Context, moduleID RegistryM url.QueryEscape(moduleID.Name), url.QueryEscape(moduleID.Provider), ) - req, err := r.client.newRequest("POST", u, nil) + req, err := r.client.NewRequest("POST", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // DeleteVersion is used to delete the specific registry module version @@ -391,12 +418,12 @@ func (r *registryModules) DeleteVersion(ctx context.Context, moduleID RegistryMo url.QueryEscape(moduleID.Provider), url.QueryEscape(version), ) - req, err := r.client.newRequest("POST", u, nil) + req, err := r.client.NewRequest("POST", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o RegistryModuleID) valid() error { @@ -420,6 +447,19 @@ func (o RegistryModuleID) valid() error { return ErrInvalidProvider } + switch o.RegistryName { + case PublicRegistry: + if !validString(&o.Namespace) { + return ErrRequiredNamespace + } + case PrivateRegistry: + case "": + // no-op: RegistryName is optional + // for all other string + default: + return ErrInvalidRegistryName + } + return nil } @@ -436,6 +476,22 @@ func (o RegistryModuleCreateOptions) valid() error { if !validStringID(o.Provider) { return ErrInvalidProvider } + + switch o.RegistryName { + case PublicRegistry: + if !validString(&o.Namespace) { + return ErrRequiredNamespace + } + case PrivateRegistry: + if validString(&o.Namespace) { + return ErrUnsupportedBothNamespaceAndPrivateRegistryName + } + case "": + // no-op: RegistryName is optional + // for all other string + default: + return ErrInvalidRegistryName + } return nil } diff --git a/registry_module_integration_test.go b/registry_module_integration_test.go index 5e4711405..bd485dba3 100644 --- a/registry_module_integration_test.go +++ b/registry_module_integration_test.go @@ -25,9 +25,9 @@ func TestRegistryModulesList(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - registryModuleTest1, registryModuleTest1Cleanup := createRegistryModule(t, client, orgTest) + registryModuleTest1, registryModuleTest1Cleanup := createRegistryModule(t, client, orgTest, PrivateRegistry) defer registryModuleTest1Cleanup() - registryModuleTest2, registryModuleTest2Cleanup := createRegistryModule(t, client, orgTest) + registryModuleTest2, registryModuleTest2Cleanup := createRegistryModule(t, client, orgTest, PrivateRegistry) defer registryModuleTest2Cleanup() t.Run("with no list options", func(t *testing.T) { @@ -71,29 +71,74 @@ func TestRegistryModulesCreate(t *testing.T) { defer orgTestCleanup() t.Run("with valid options", func(t *testing.T) { - options := RegistryModuleCreateOptions{ - Name: String("name"), - Provider: String("provider"), + assertRegistryModuleAttributes := func(t *testing.T, registryModule *RegistryModule) { + t.Run("permissions are properly decoded", func(t *testing.T) { + require.NotEmpty(t, registryModule.Permissions) + assert.True(t, registryModule.Permissions.CanDelete) + assert.True(t, registryModule.Permissions.CanResync) + assert.True(t, registryModule.Permissions.CanRetry) + }) + + t.Run("relationships are properly decoded", func(t *testing.T) { + require.NotEmpty(t, registryModule.Organization) + assert.Equal(t, orgTest.Name, registryModule.Organization.Name) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, registryModule.CreatedAt) + assert.NotEmpty(t, registryModule.UpdatedAt) + }) } - rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) - require.NoError(t, err) - assert.NotEmpty(t, rm.ID) - assert.Equal(t, *options.Name, rm.Name) - assert.Equal(t, *options.Provider, rm.Provider) - t.Run("permissions are properly decoded", func(t *testing.T) { - assert.True(t, rm.Permissions.CanDelete) - assert.True(t, rm.Permissions.CanResync) - assert.True(t, rm.Permissions.CanRetry) + t.Run("without RegistryName", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("name"), + Provider: String("provider"), + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + assert.NotEmpty(t, rm.ID) + assert.Equal(t, *options.Name, rm.Name) + assert.Equal(t, *options.Provider, rm.Provider) + assert.Equal(t, PrivateRegistry, rm.RegistryName) + assert.Equal(t, orgTest.Name, rm.Namespace) + + assertRegistryModuleAttributes(t, rm) }) - t.Run("relationships are properly decoded", func(t *testing.T) { - assert.Equal(t, orgTest.Name, rm.Organization.Name) + t.Run("with private RegistryName", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("another_name"), + Provider: String("provider"), + RegistryName: PrivateRegistry, + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + assert.NotEmpty(t, rm.ID) + assert.Equal(t, *options.Name, rm.Name) + assert.Equal(t, *options.Provider, rm.Provider) + assert.Equal(t, options.RegistryName, rm.RegistryName) + assert.Equal(t, orgTest.Name, rm.Namespace) + + assertRegistryModuleAttributes(t, rm) }) - t.Run("timestamps are properly decoded", func(t *testing.T) { - assert.NotEmpty(t, rm.CreatedAt) - assert.NotEmpty(t, rm.UpdatedAt) + t.Run("with public RegistryName", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("vpc"), + Provider: String("aws"), + RegistryName: PublicRegistry, + Namespace: "terraform-aws-modules", + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + assert.NotEmpty(t, rm.ID) + assert.Equal(t, *options.Name, rm.Name) + assert.Equal(t, *options.Provider, rm.Provider) + assert.Equal(t, options.RegistryName, rm.RegistryName) + assert.Equal(t, options.Namespace, rm.Namespace) + + assertRegistryModuleAttributes(t, rm) }) }) @@ -135,6 +180,40 @@ func TestRegistryModulesCreate(t *testing.T) { assert.Nil(t, rm) assert.Equal(t, err, ErrInvalidProvider) }) + + t.Run("with an invalid registry name", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("name"), + Provider: String("provider"), + RegistryName: "PRIVATE", + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.Equal(t, err, ErrInvalidRegistryName) + }) + + t.Run("without a namespace for public registry name", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("name"), + Provider: String("provider"), + RegistryName: PublicRegistry, + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.Equal(t, err, ErrRequiredNamespace) + }) + + t.Run("with a namespace for private registry name", func(t *testing.T) { + options := RegistryModuleCreateOptions{ + Name: String("name"), + Provider: String("provider"), + RegistryName: PrivateRegistry, + Namespace: "namespace", + } + rm, err := client.RegistryModules.Create(ctx, orgTest.Name, options) + assert.Nil(t, rm) + assert.Equal(t, err, ErrUnsupportedBothNamespaceAndPrivateRegistryName) + }) }) t.Run("without a valid organization", func(t *testing.T) { @@ -155,7 +234,7 @@ func TestRegistryModulesCreateVersion(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest) + registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry) defer registryModuleTestCleanup() t.Run("with valid options", func(t *testing.T) { @@ -393,9 +472,12 @@ func TestRegistryModulesRead(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest) + registryModuleTest, registryModuleTestCleanup := createRegistryModule(t, client, orgTest, PrivateRegistry) defer registryModuleTestCleanup() + publicRegistryModuleTest, publicRegistryModuleTestCleanup := createRegistryModule(t, client, orgTest, PublicRegistry) + defer publicRegistryModuleTestCleanup() + t.Run("with valid name and provider", func(t *testing.T) { rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ Organization: orgTest.Name, @@ -417,6 +499,56 @@ func TestRegistryModulesRead(t *testing.T) { }) }) + t.Run("with complete registry module ID fields for private module", func(t *testing.T) { + rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ + Organization: orgTest.Name, + Name: registryModuleTest.Name, + Provider: registryModuleTest.Provider, + Namespace: orgTest.Name, + RegistryName: PrivateRegistry, + }) + require.NoError(t, err) + require.NotEmpty(t, rm) + assert.Equal(t, registryModuleTest.ID, rm.ID) + + t.Run("permissions are properly decoded", func(t *testing.T) { + require.NotEmpty(t, rm.Permissions) + assert.True(t, rm.Permissions.CanDelete) + assert.True(t, rm.Permissions.CanResync) + assert.True(t, rm.Permissions.CanRetry) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, rm.CreatedAt) + assert.NotEmpty(t, rm.UpdatedAt) + }) + }) + + t.Run("with complete registry module ID fields for public module", func(t *testing.T) { + rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ + Organization: orgTest.Name, + Name: publicRegistryModuleTest.Name, + Provider: publicRegistryModuleTest.Provider, + Namespace: publicRegistryModuleTest.Namespace, + RegistryName: PublicRegistry, + }) + require.NoError(t, err) + require.NotEmpty(t, rm) + assert.Equal(t, publicRegistryModuleTest.ID, rm.ID) + + t.Run("permissions are properly decoded", func(t *testing.T) { + require.NotEmpty(t, rm.Permissions) + assert.True(t, rm.Permissions.CanDelete) + assert.True(t, rm.Permissions.CanResync) + assert.True(t, rm.Permissions.CanRetry) + }) + + t.Run("timestamps are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, rm.CreatedAt) + assert.NotEmpty(t, rm.UpdatedAt) + }) + }) + t.Run("without a name", func(t *testing.T) { rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ Organization: orgTest.Name, @@ -457,6 +589,18 @@ func TestRegistryModulesRead(t *testing.T) { assert.Equal(t, err, ErrInvalidProvider) }) + t.Run("with an invalid registry name", func(t *testing.T) { + rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ + Organization: orgTest.Name, + Name: registryModuleTest.Name, + Provider: registryModuleTest.Provider, + Namespace: orgTest.Name, + RegistryName: "PRIVATE", + }) + assert.Nil(t, rm) + assert.Equal(t, err, ErrInvalidRegistryName) + }) + t.Run("without a valid organization", func(t *testing.T) { rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ Organization: badIdentifier, @@ -467,6 +611,17 @@ func TestRegistryModulesRead(t *testing.T) { assert.EqualError(t, err, ErrInvalidOrg.Error()) }) + t.Run("without a valid namespace for public registry module", func(t *testing.T) { + rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ + Organization: orgTest.Name, + Name: publicRegistryModuleTest.Name, + Provider: publicRegistryModuleTest.Provider, + RegistryName: PublicRegistry, + }) + assert.Nil(t, rm) + assert.EqualError(t, err, ErrRequiredNamespace.Error()) + }) + t.Run("when the registry module does not exist", func(t *testing.T) { rm, err := client.RegistryModules.Read(ctx, RegistryModuleID{ Organization: orgTest.Name, @@ -485,7 +640,7 @@ func TestRegistryModulesDelete(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - registryModuleTest, _ := createRegistryModule(t, client, orgTest) + registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry) t.Run("with valid name", func(t *testing.T) { err := client.RegistryModules.Delete(ctx, orgTest.Name, registryModuleTest.Name) @@ -529,7 +684,7 @@ func TestRegistryModulesDeleteProvider(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - registryModuleTest, _ := createRegistryModule(t, client, orgTest) + registryModuleTest, _ := createRegistryModule(t, client, orgTest, PrivateRegistry) t.Run("with valid name and provider", func(t *testing.T) { err := client.RegistryModules.DeleteProvider(ctx, RegistryModuleID{ @@ -734,7 +889,7 @@ func TestRegistryModulesUpload(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - rm, _ := createRegistryModule(t, client, orgTest) + rm, _ := createRegistryModule(t, client, orgTest, PrivateRegistry) optionsModuleVersion := RegistryModuleCreateVersionOptions{ Version: String("1.0.0"), @@ -775,8 +930,10 @@ func TestRegistryModule_Unmarshal(t *testing.T) { "type": "registry-modules", "id": "1", "attributes": map[string]interface{}{ - "name": "module", - "provider": "tfe", + "name": "module", + "provider": "tfe", + "namespace": "org-abc", + "registry-name": "private", "permissions": map[string]interface{}{ "can-delete": true, "can-resync": true, @@ -815,6 +972,8 @@ func TestRegistryModule_Unmarshal(t *testing.T) { assert.Equal(t, rm.ID, "1") assert.Equal(t, rm.Name, "module") assert.Equal(t, rm.Provider, "tfe") + assert.Equal(t, rm.Namespace, "org-abc") + assert.Equal(t, rm.RegistryName, PrivateRegistry) assert.Equal(t, rm.Permissions.CanDelete, true) assert.Equal(t, rm.Permissions.CanRetry, true) assert.Equal(t, rm.Status, RegistryModuleStatusPending) @@ -832,7 +991,7 @@ func TestRegistryModule_Unmarshal(t *testing.T) { assert.Equal(t, rm.VersionStatuses[0].Error, "no error") } -func TestRegistryCreateOptions_Marshal(t *testing.T) { +func TestRegistryCreateWithVCSOptions_Marshal(t *testing.T) { // https://www.terraform.io/docs/cloud/api/modules.html#sample-payload opts := RegistryModuleCreateWithVCSConnectionOptions{ VCSRepo: &RegistryModuleVCSRepoOptions{ diff --git a/registry_provider.go b/registry_provider.go index db32da997..43dd9fced 100644 --- a/registry_provider.go +++ b/registry_provider.go @@ -132,13 +132,13 @@ func (r *registryProviders) List(ctx context.Context, organization string, optio } u := fmt.Sprintf("organizations/%s/registry-providers", url.QueryEscape(organization)) - req, err := r.client.newRequest("GET", u, options) + req, err := r.client.NewRequest("GET", u, options) if err != nil { return nil, err } pl := &RegistryProviderList{} - err = r.client.do(ctx, req, pl) + err = req.Do(ctx, pl) if err != nil { return nil, err } @@ -159,13 +159,13 @@ func (r *registryProviders) Create(ctx context.Context, organization string, opt "organizations/%s/registry-providers", url.QueryEscape(organization), ) - req, err := r.client.newRequest("POST", u, &options) + req, err := r.client.NewRequest("POST", u, &options) if err != nil { return nil, err } prv := &RegistryProvider{} - err = r.client.do(ctx, req, prv) + err = req.Do(ctx, prv) if err != nil { return nil, err } @@ -185,13 +185,13 @@ func (r *registryProviders) Read(ctx context.Context, providerID RegistryProvide url.QueryEscape(providerID.Namespace), url.QueryEscape(providerID.Name), ) - req, err := r.client.newRequest("GET", u, options) + req, err := r.client.NewRequest("GET", u, options) if err != nil { return nil, err } prv := &RegistryProvider{} - err = r.client.do(ctx, req, prv) + err = req.Do(ctx, prv) if err != nil { return nil, err } @@ -211,12 +211,12 @@ func (r *registryProviders) Delete(ctx context.Context, providerID RegistryProvi url.QueryEscape(providerID.Namespace), url.QueryEscape(providerID.Name), ) - req, err := r.client.newRequest("DELETE", u, nil) + req, err := r.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o RegistryProviderCreateOptions) valid() error { diff --git a/registry_provider_integration_test.go b/registry_provider_integration_test.go index 3e53cf292..ba8253df3 100644 --- a/registry_provider_integration_test.go +++ b/registry_provider_integration_test.go @@ -136,7 +136,9 @@ func TestRegistryProvidersList(t *testing.T) { } providersRead, err := client.RegistryProviders.List(ctx, provider.Organization.Name, &options) - assert.NoError(t, err) + require.NoError(t, err) + + require.NotEmpty(t, providersRead.Items) providerRead := providersRead.Items[0] assert.Equal(t, providerRead.ID, provider.ID) assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions)) @@ -316,7 +318,8 @@ func TestRegistryProvidersRead(t *testing.T) { } prv, err := client.RegistryProviders.Read(ctx, id, nil) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotEmpty(t, prv.ID) assert.Equal(t, registryProviderTest.Name, prv.Name) assert.Equal(t, registryProviderTest.Namespace, prv.Namespace) @@ -377,7 +380,9 @@ func TestRegistryProvidersRead(t *testing.T) { } providerRead, err := client.RegistryProviders.Read(ctx, id, &options) - assert.NoError(t, err) + require.NoError(t, err) + require.NotEmpty(t, providerRead.RegistryProviderVersions) + assert.Equal(t, providerRead.ID, provider.ID) assert.Equal(t, len(versions), len(providerRead.RegistryProviderVersions)) foundVersion := &RegistryProviderVersion{} @@ -463,7 +468,7 @@ func TestRegistryProvidersIDValidation(t *testing.T) { Namespace: "namespace", Name: "name", } - assert.NoError(t, id.valid()) + require.NoError(t, id.valid()) }) t.Run("without a name", func(t *testing.T) { diff --git a/registry_provider_platform.go b/registry_provider_platform.go index 9f2d99756..c64ce84bc 100644 --- a/registry_provider_platform.go +++ b/registry_provider_platform.go @@ -56,13 +56,13 @@ type RegistryProviderPlatformID struct { // RegistryProviderPlatformCreateOptions represents the set of options for creating a registry provider platform type RegistryProviderPlatformCreateOptions struct { // Required: A valid operating system string - OS string `jsonapi:"attr,os"` + OS string `jsonapi:"attr,os"` // Required: A valid architecture string - Arch string `jsonapi:"attr,arch"` + Arch string `jsonapi:"attr,arch"` // Required: A valid shasum string - Shasum string `jsonapi:"attr,shasum"` + Shasum string `jsonapi:"attr,shasum"` // Required: A valid filename string Filename string `jsonapi:"attr,filename"` @@ -96,13 +96,13 @@ func (r *registryProviderPlatforms) Create(ctx context.Context, versionID Regist url.QueryEscape(versionID.Name), url.QueryEscape(versionID.Version), ) - req, err := r.client.newRequest("POST", u, &options) + req, err := r.client.NewRequest("POST", u, &options) if err != nil { return nil, err } rpp := &RegistryProviderPlatform{} - err = r.client.do(ctx, req, rpp) + err = req.Do(ctx, rpp) if err != nil { return nil, err } @@ -128,13 +128,13 @@ func (r *registryProviderPlatforms) List(ctx context.Context, versionID Registry url.QueryEscape(versionID.RegistryProviderID.Name), url.QueryEscape(versionID.Version), ) - req, err := r.client.newRequest("GET", u, options) + req, err := r.client.NewRequest("GET", u, options) if err != nil { return nil, err } ppl := &RegistryProviderPlatformList{} - err = r.client.do(ctx, req, ppl) + err = req.Do(ctx, ppl) if err != nil { return nil, err } @@ -159,13 +159,13 @@ func (r *registryProviderPlatforms) Read(ctx context.Context, platformID Registr url.QueryEscape(platformID.OS), url.QueryEscape(platformID.Arch), ) - req, err := r.client.newRequest("GET", u, nil) + req, err := r.client.NewRequest("GET", u, nil) if err != nil { return nil, err } rpp := &RegistryProviderPlatform{} - err = r.client.do(ctx, req, rpp) + err = req.Do(ctx, rpp) if err != nil { return nil, err @@ -191,12 +191,12 @@ func (r *registryProviderPlatforms) Delete(ctx context.Context, platformID Regis url.QueryEscape(platformID.OS), url.QueryEscape(platformID.Arch), ) - req, err := r.client.newRequest("DELETE", u, nil) + req, err := r.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (id RegistryProviderPlatformID) valid() error { diff --git a/registry_provider_platform_integration_test.go b/registry_provider_platform_integration_test.go index b82f44f55..95e322199 100644 --- a/registry_provider_platform_integration_test.go +++ b/registry_provider_platform_integration_test.go @@ -195,7 +195,7 @@ func TestRegistryProviderPlatformsDelete(t *testing.T) { } err := client.RegistryProviderPlatforms.Delete(ctx, platformID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with a non-existant version", func(t *testing.T) { @@ -243,7 +243,8 @@ func TestRegistryProviderPlatformsRead(t *testing.T) { } readPlatform, err := client.RegistryProviderPlatforms.Read(ctx, platformID) - assert.NoError(t, err) + require.NoError(t, err) + assert.Equal(t, platformID.OS, readPlatform.OS) assert.Equal(t, platformID.Arch, readPlatform.Arch) assert.Equal(t, platform.Filename, readPlatform.Filename) diff --git a/registry_provider_version.go b/registry_provider_version.go index f84554854..57c696156 100644 --- a/registry_provider_version.go +++ b/registry_provider_version.go @@ -74,10 +74,10 @@ type RegistryProviderVersionListOptions struct { type RegistryProviderVersionCreateOptions struct { // Required: A valid semver version string. - Version string `jsonapi:"attr,version"` + Version string `jsonapi:"attr,version"` // Required: A valid gpg-key string. - KeyID string `jsonapi:"attr,key-id"` + KeyID string `jsonapi:"attr,key-id"` // Required: An array of Terraform provider API versions that this version supports. Protocols []string `jsonapi:"attr,protocols"` @@ -99,13 +99,13 @@ func (r *registryProviderVersions) List(ctx context.Context, providerID Registry url.QueryEscape(providerID.Namespace), url.QueryEscape(providerID.Name), ) - req, err := r.client.newRequest("GET", u, options) + req, err := r.client.NewRequest("GET", u, options) if err != nil { return nil, err } pvl := &RegistryProviderVersionList{} - err = r.client.do(ctx, req, pvl) + err = req.Do(ctx, pvl) if err != nil { return nil, err } @@ -134,13 +134,13 @@ func (r *registryProviderVersions) Create(ctx context.Context, providerID Regist url.QueryEscape(providerID.Namespace), url.QueryEscape(providerID.Name), ) - req, err := r.client.newRequest("POST", u, &options) + req, err := r.client.NewRequest("POST", u, &options) if err != nil { return nil, err } prvv := &RegistryProviderVersion{} - err = r.client.do(ctx, req, prvv) + err = req.Do(ctx, prvv) if err != nil { return nil, err } @@ -162,13 +162,13 @@ func (r *registryProviderVersions) Read(ctx context.Context, versionID RegistryP url.QueryEscape(versionID.Name), url.QueryEscape(versionID.Version), ) - req, err := r.client.newRequest("GET", u, nil) + req, err := r.client.NewRequest("GET", u, nil) if err != nil { return nil, err } prvv := &RegistryProviderVersion{} - err = r.client.do(ctx, req, prvv) + err = req.Do(ctx, prvv) if err != nil { return nil, err } @@ -190,12 +190,12 @@ func (r *registryProviderVersions) Delete(ctx context.Context, versionID Registr url.QueryEscape(versionID.Name), url.QueryEscape(versionID.Version), ) - req, err := r.client.newRequest("DELETE", u, nil) + req, err := r.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return r.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // ShasumsUploadURL returns the upload URL to upload shasums if one is available diff --git a/registry_provider_version_integration_test.go b/registry_provider_version_integration_test.go index 8bd6f4266..7dc5f8d98 100644 --- a/registry_provider_version_integration_test.go +++ b/registry_provider_version_integration_test.go @@ -38,7 +38,7 @@ func TestRegistryProviderVersionsIDValidation(t *testing.T) { Version: version, RegistryProviderID: validRegistryProviderId, } - assert.NoError(t, id.valid()) + require.NoError(t, id.valid()) }) t.Run("without a version", func(t *testing.T) { @@ -121,9 +121,9 @@ func TestRegistryProviderVersionsCreate(t *testing.T) { t.Run("includes upload links", func(t *testing.T) { _, err := prvv.ShasumsUploadURL() - assert.NoError(t, err) + require.NoError(t, err) _, err = prvv.ShasumsSigUploadURL() - assert.NoError(t, err) + require.NoError(t, err) expectedLinks := []string{ "shasums-upload", "shasums-sig-upload", @@ -229,7 +229,7 @@ func TestRegistryProviderVersionsList(t *testing.T) { }, }) require.NoError(t, err) - assert.NotEmpty(t, returnedVersions.Items) + require.NotEmpty(t, returnedVersions.Items) assert.Equal(t, versionN, returnedVersions.TotalCount) assert.Equal(t, 1, returnedVersions.TotalPages) for _, rv := range returnedVersions.Items { @@ -258,7 +258,8 @@ func TestRegistryProviderVersionsList(t *testing.T) { }, }) require.NoError(t, err) - assert.NotEmpty(t, returnedVersions.Items) + require.NotEmpty(t, returnedVersions.Items) + assert.Equal(t, versionN, returnedVersions.TotalCount) assert.Equal(t, pageN, returnedVersions.TotalPages) assert.Equal(t, pageSize, len(returnedVersions.Items)) @@ -321,7 +322,7 @@ func TestRegistryProviderVersionsDelete(t *testing.T) { } err := client.RegistryProviderVersions.Delete(ctx, versionID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with non existing version", func(t *testing.T) { @@ -359,7 +360,7 @@ func TestRegistryProviderVersionsRead(t *testing.T) { } readVersion, err := client.RegistryProviderVersions.Read(ctx, versionID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, version.ID, readVersion.ID) assert.Equal(t, version.Version, readVersion.Version) assert.Equal(t, version.KeyID, readVersion.KeyID) diff --git a/request.go b/request.go new file mode 100644 index 000000000..a84ccfc37 --- /dev/null +++ b/request.go @@ -0,0 +1,104 @@ +package tfe + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + retryablehttp "github.com/hashicorp/go-retryablehttp" + "golang.org/x/time/rate" +) + +// ClientRequest encapsulates a request sent by the Client +type ClientRequest struct { + retryableRequest *retryablehttp.Request + http *retryablehttp.Client + limiter *rate.Limiter + + // Header are the headers that will be sent in this request + Header http.Header +} + +func (r ClientRequest) Do(ctx context.Context, model interface{}) error { + // Wait will block until the limiter can obtain a new token + // or returns an error if the given context is canceled. + if err := r.limiter.Wait(ctx); err != nil { + return err + } + + // Add the context to the request. + reqWithCxt := r.retryableRequest.WithContext(ctx) + + // Execute the request and check the response. + resp, err := r.http.Do(reqWithCxt) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return ctx.Err() + default: + return err + } + } + defer resp.Body.Close() + + // Basic response checking. + if err := checkResponseCode(resp); err != nil { + return err + } + + // Return here if decoding the response isn't needed. + if model == nil { + return nil + } + + // If v implements io.Writer, write the raw response body. + if w, ok := model.(io.Writer); ok { + _, err := io.Copy(w, resp.Body) + return err + } + + return unmarshalResponse(resp.Body, model) +} + +// doIpRanges is similar to Do except that The IP ranges API is not returning jsonapi +// like every other endpoint which means we need to handle it differently. +func (r *ClientRequest) doIpRanges(ctx context.Context, ir *IPRange) error { + // Wait will block until the limiter can obtain a new token + // or returns an error if the given context is canceled. + if err := r.limiter.Wait(ctx); err != nil { + return err + } + + // Add the context to the request. + contextReq := r.retryableRequest.WithContext(ctx) + + // Execute the request and check the response. + resp, err := r.http.Do(contextReq) + if err != nil { + // If we got an error, and the context has been canceled, + // the context's error is probably more useful. + select { + case <-ctx.Done(): + return ctx.Err() + default: + return err + } + } + defer resp.Body.Close() + + if resp.StatusCode < 200 && resp.StatusCode >= 400 { + return fmt.Errorf("error HTTP response while retrieving IP ranges: %d", resp.StatusCode) + } else if resp.StatusCode == 304 { + return nil + } + + err = json.NewDecoder(resp.Body).Decode(ir) + if err != nil { + return err + } + return nil +} diff --git a/run.go b/run.go index 0e67486dc..93b90d890 100644 --- a/run.go +++ b/run.go @@ -325,13 +325,13 @@ func (s *runs) List(ctx context.Context, workspaceID string, options *RunListOpt } u := fmt.Sprintf("workspaces/%s/runs", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } rl := &RunList{} - err = s.client.do(ctx, req, rl) + err = req.Do(ctx, rl) if err != nil { return nil, err } @@ -345,13 +345,13 @@ func (s *runs) Create(ctx context.Context, options RunCreateOptions) (*Run, erro return nil, err } - req, err := s.client.newRequest("POST", "runs", &options) + req, err := s.client.NewRequest("POST", "runs", &options) if err != nil { return nil, err } r := &Run{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -374,13 +374,13 @@ func (s *runs) ReadWithOptions(ctx context.Context, runID string, options *RunRe } u := fmt.Sprintf("runs/%s", url.QueryEscape(runID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } r := &Run{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -395,12 +395,12 @@ func (s *runs) Apply(ctx context.Context, runID string, options RunApplyOptions) } u := fmt.Sprintf("runs/%s/actions/apply", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Cancel a run by its ID. @@ -410,12 +410,12 @@ func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOption } u := fmt.Sprintf("runs/%s/actions/cancel", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // ForceCancel is used to forcefully cancel a run by its ID. @@ -425,12 +425,12 @@ func (s *runs) ForceCancel(ctx context.Context, runID string, options RunForceCa } u := fmt.Sprintf("runs/%s/actions/force-cancel", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Discard a run by its ID. @@ -440,12 +440,12 @@ func (s *runs) Discard(ctx context.Context, runID string, options RunDiscardOpti } u := fmt.Sprintf("runs/%s/actions/discard", url.QueryEscape(runID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o RunCreateOptions) valid() error { diff --git a/run_integration_test.go b/run_integration_test.go index 9ecd56ef7..ee647d29d 100644 --- a/run_integration_test.go +++ b/run_integration_test.go @@ -45,6 +45,7 @@ func TestRunsList(t *testing.T) { Include: []RunIncludeOpt{}, }) require.NoError(t, err) + require.NotEmpty(t, rl.Items) found := []string{} for _, r := range rl.Items { @@ -79,11 +80,10 @@ func TestRunsList(t *testing.T) { rl, err := client.Runs.List(ctx, wTest.ID, &RunListOptions{ Include: []RunIncludeOpt{RunWorkspace}, }) + require.NoError(t, err) - assert.NoError(t, err) - - assert.NotEmpty(t, rl.Items) - assert.NotNil(t, rl.Items[0].Workspace) + require.NotEmpty(t, rl.Items) + require.NotNil(t, rl.Items[0].Workspace) assert.NotEmpty(t, rl.Items[0].Workspace.Name) }) @@ -118,7 +118,7 @@ func TestRunsListQueryParams(t *testing.T) { description: "with status query parameter", options: &RunListOptions{Status: string(RunPending), Include: []RunIncludeOpt{RunWorkspace}}, assertion: func(tc testCase, rl *RunList, err error) { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 1, len(rl.Items)) }, }, @@ -126,7 +126,7 @@ func TestRunsListQueryParams(t *testing.T) { description: "with source query parameter", options: &RunListOptions{Source: string(RunSourceAPI), Include: []RunIncludeOpt{RunWorkspace}}, assertion: func(tc testCase, rl *RunList, err error) { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2, len(rl.Items)) assert.Equal(t, rl.Items[0].Source, RunSourceAPI) }, @@ -135,7 +135,7 @@ func TestRunsListQueryParams(t *testing.T) { description: "with operation of plan_only parameter", options: &RunListOptions{Operation: string(RunOperationPlanOnly), Include: []RunIncludeOpt{RunWorkspace}}, assertion: func(tc testCase, rl *RunList, err error) { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 0, len(rl.Items)) }, }, @@ -155,7 +155,7 @@ func TestRunsListQueryParams(t *testing.T) { description: "with name & commit parameter", options: &RunListOptions{Name: randomString(t), Commit: randomString(t), Include: []RunIncludeOpt{RunWorkspace}}, assertion: func(tc testCase, rl *RunList, err error) { - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 0, len(rl.Items)) }, }, @@ -185,11 +185,11 @@ func TestRunsCreate(t *testing.T) { } r, err := client.Runs.Create(ctx, options) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, r.ID) assert.NotNil(t, r.CreatedAt) assert.NotNil(t, r.Source) - assert.NotEmpty(t, r.StatusTimestamps) + require.NotNil(t, r.StatusTimestamps) assert.NotZero(t, r.StatusTimestamps.PlanQueueableAt) }) @@ -323,7 +323,7 @@ func TestRunsRead_CostEstimate(t *testing.T) { t.Run("when the run exists", func(t *testing.T) { r, err := client.Runs.Read(ctx, rTest.ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, rTest, r) }) @@ -355,7 +355,7 @@ func TestRunsReadWithOptions(t *testing.T) { r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, curOpts) require.NoError(t, err) - assert.NotEmpty(t, r.CreatedBy) + require.NotEmpty(t, r.CreatedBy) assert.NotEmpty(t, r.CreatedBy.Username) }) } @@ -374,15 +374,15 @@ func TestRunsApply(t *testing.T) { err := client.Runs.Apply(ctx, rTest.ID, RunApplyOptions{ Comment: String("Hello, Earl"), }) - assert.NoError(t, err) + require.NoError(t, err) r, err := client.Runs.Read(ctx, rTest.ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, r.Comments, 1) c, err := client.Comments.Read(ctx, r.Comments[0].ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "Hello, Earl", c.Body) }) @@ -414,7 +414,7 @@ func TestRunsCancel(t *testing.T) { t.Run("when the run exists", func(t *testing.T) { err := client.Runs.Cancel(ctx, rTest.ID, RunCancelOptions{}) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("when the run does not exist", func(t *testing.T) { @@ -507,7 +507,7 @@ func TestRunsDiscard(t *testing.T) { t.Run("when the run exists", func(t *testing.T) { err := client.Runs.Discard(ctx, rTest.ID, RunDiscardOptions{}) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("when the run does not exist", func(t *testing.T) { diff --git a/run_task.go b/run_task.go index ffbaf0d17..12f87f1b1 100644 --- a/run_task.go +++ b/run_task.go @@ -11,8 +11,7 @@ var _ RunTasks = (*runTasks)(nil) // RunTasks represents all the run task related methods in the context of an organization // that the Terraform Cloud/Enterprise API supports. -// **Note: This API is still in BETA and subject to change.** -// https://www.terraform.io/cloud-docs/api-docs/run-tasks#run-tasks-api +// https://www.terraform.io/cloud-docs/api-docs/run-tasks/run-tasks#run-tasks-api type RunTasks interface { // Create a run task for an organization Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) @@ -43,12 +42,13 @@ type runTasks struct { // RunTask represents a TFC/E run task type RunTask struct { - ID string `jsonapi:"primary,tasks"` - Name string `jsonapi:"attr,name"` - URL string `jsonapi:"attr,url"` - Category string `jsonapi:"attr,category"` - HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` - Enabled bool `jsonapi:"attr,enabled"` + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Description string `jsonapi:"attr,description"` + Category string `jsonapi:"attr,category"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` + Enabled bool `jsonapi:"attr,enabled"` Organization *Organization `jsonapi:"relation,organization"` WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"` @@ -61,7 +61,7 @@ type RunTaskList struct { } // RunTaskIncludeOpt represents the available options for include query params. -// https://www.terraform.io/cloud-docs/api-docs/run-tasks#list-run-tasks +// https://www.terraform.io/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks type RunTaskIncludeOpt string const ( @@ -73,14 +73,14 @@ const ( type RunTaskListOptions struct { ListOptions // Optional: A list of relations to include with a run task. See available resources: - // https://www.terraform.io/cloud-docs/api-docs/run-tasks#list-run-tasks + // https://www.terraform.io/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks Include []RunTaskIncludeOpt `url:"include,omitempty"` } // RunTaskReadOptions represents the set of options for reading a run task type RunTaskReadOptions struct { // Optional: A list of relations to include with a run task. See available resources: - // https://www.terraform.io/cloud-docs/api-docs/run-tasks#list-run-tasks + // https://www.terraform.io/cloud-docs/api-docs/run-tasks/run-tasks#list-run-tasks Include []RunTaskIncludeOpt `url:"include,omitempty"` } @@ -98,6 +98,9 @@ type RunTaskCreateOptions struct { // Required: The URL to send a run task payload URL string `jsonapi:"attr,url"` + // Optional: Description of the task + Description *string `jsonapi:"attr,description"` + // Required: Must be "task" Category string `jsonapi:"attr,category"` @@ -122,6 +125,9 @@ type RunTaskUpdateOptions struct { // Optional: The URL to send a run task payload, defaults to previous value URL *string `jsonapi:"attr,url,omitempty"` + // Optional: An optional description of the task + Description *string `jsonapi:"attr,description,omitempty"` + // Optional: Must be "task", defaults to "task" Category *string `jsonapi:"attr,category,omitempty"` @@ -143,13 +149,13 @@ func (s *runTasks) Create(ctx context.Context, organization string, options RunT } u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } r := &RunTask{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -167,13 +173,13 @@ func (s *runTasks) List(ctx context.Context, organization string, options *RunTa } u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } rl := &RunTaskList{} - err = s.client.do(ctx, req, rl) + err = req.Do(ctx, rl) if err != nil { return nil, err } @@ -196,13 +202,13 @@ func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, option } u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } r := &RunTask{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -221,13 +227,13 @@ func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTask } u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } r := &RunTask{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -242,12 +248,12 @@ func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { } u := fmt.Sprintf("tasks/%s", runTaskID) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // AttachToWorkspace is a convenient method to attach a run task to a workspace. See: WorkspaceRunTasks.Create() diff --git a/run_task_integration_test.go b/run_task_integration_test.go index e9d08a4d3..5448eaad7 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -13,7 +13,7 @@ import ( ) func TestRunTasksCreate(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -27,13 +27,15 @@ func TestRunTasksCreate(t *testing.T) { } runTaskName := "tst-runtask-" + randomString(t) + runTaskDescription := "A Run Task Description" t.Run("add run task to organization", func(t *testing.T) { r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{ - Name: runTaskName, - URL: runTaskServerURL, - Category: "task", - Enabled: Bool(true), + Name: runTaskName, + URL: runTaskServerURL, + Description: &runTaskDescription, + Category: "task", + Enabled: Bool(true), }) require.NoError(t, err) @@ -41,6 +43,7 @@ func TestRunTasksCreate(t *testing.T) { assert.Equal(t, r.Name, runTaskName) assert.Equal(t, r.URL, runTaskServerURL) assert.Equal(t, r.Category, "task") + assert.Equal(t, r.Description, runTaskDescription) t.Run("ensure org is deserialized properly", func(t *testing.T) { assert.Equal(t, r.Organization.Name, orgTest.Name) @@ -49,7 +52,7 @@ func TestRunTasksCreate(t *testing.T) { } func TestRunTasksList(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -75,7 +78,7 @@ func TestRunTasksList(t *testing.T) { } func TestRunTasksRead(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -93,6 +96,7 @@ func TestRunTasksRead(t *testing.T) { assert.Equal(t, runTaskTest.ID, r.ID) assert.Equal(t, runTaskTest.URL, r.URL) assert.Equal(t, runTaskTest.Category, r.Category) + assert.Equal(t, runTaskTest.Description, r.Description) assert.Equal(t, runTaskTest.HMACKey, r.HMACKey) assert.Equal(t, runTaskTest.Enabled, r.Enabled) }) @@ -116,7 +120,7 @@ func TestRunTasksRead(t *testing.T) { require.NoError(t, err) - assert.NotEmpty(t, r.WorkspaceRunTasks) + require.NotEmpty(t, r.WorkspaceRunTasks) assert.NotEmpty(t, r.WorkspaceRunTasks[0].ID) assert.NotEmpty(t, r.WorkspaceRunTasks[0].EnforcementLevel) assert.NotEmpty(t, r.WorkspaceRunTasks[1].ID) @@ -125,7 +129,7 @@ func TestRunTasksRead(t *testing.T) { } func TestRunTasksUpdate(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -148,10 +152,36 @@ func TestRunTasksUpdate(t *testing.T) { assert.Equal(t, rename, r.Name) }) + + t.Run("toggle enabled", func(t *testing.T) { + runTaskTest.Enabled = !runTaskTest.Enabled + r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{ + Enabled: &runTaskTest.Enabled, + }) + require.NoError(t, err) + + r, err = client.RunTasks.Read(ctx, r.ID) + require.NoError(t, err) + + assert.Equal(t, runTaskTest.Enabled, r.Enabled) + }) + + t.Run("update description", func(t *testing.T) { + newDescription := "An updated task description" + r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{ + Description: &newDescription, + }) + require.NoError(t, err) + + r, err = client.RunTasks.Read(ctx, r.ID) + require.NoError(t, err) + + assert.Equal(t, newDescription, r.Description) + }) } func TestRunTasksDelete(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -181,7 +211,7 @@ func TestRunTasksDelete(t *testing.T) { } func TestRunTasksAttachToWorkspace(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -197,6 +227,11 @@ func TestRunTasksAttachToWorkspace(t *testing.T) { t.Run("to a valid workspace", func(t *testing.T) { wr, err := client.RunTasks.AttachToWorkspace(ctx, wkspaceTest.ID, runTaskTest.ID, Advisory) + + defer func() { + client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wr.ID) + }() + require.NoError(t, err) require.NotNil(t, wr.ID) }) diff --git a/run_trigger.go b/run_trigger.go index f0c77730c..f16173f5c 100644 --- a/run_trigger.go +++ b/run_trigger.go @@ -101,13 +101,13 @@ func (s *runTriggers) List(ctx context.Context, workspaceID string, options *Run } u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } rtl := &RunTriggerList{} - err = s.client.do(ctx, req, rtl) + err = req.Do(ctx, rtl) if err != nil { return nil, err } @@ -125,13 +125,13 @@ func (s *runTriggers) Create(ctx context.Context, workspaceID string, options Ru } u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } rt := &RunTrigger{} - err = s.client.do(ctx, req, rt) + err = req.Do(ctx, rt) if err != nil { return nil, err } @@ -146,13 +146,13 @@ func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigge } u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } rt := &RunTrigger{} - err = s.client.do(ctx, req, rt) + err = req.Do(ctx, rt) if err != nil { return nil, err } @@ -167,12 +167,12 @@ func (s *runTriggers) Delete(ctx context.Context, runTriggerID string) error { } u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o RunTriggerCreateOptions) valid() error { diff --git a/run_trigger_integration_test.go b/run_trigger_integration_test.go index 8d65eda82..5e8d7cafb 100644 --- a/run_trigger_integration_test.go +++ b/run_trigger_integration_test.go @@ -127,7 +127,8 @@ func TestRunTriggerList(t *testing.T) { }, ) require.NoError(t, err) - assert.NotEmpty(t, rtl.Items) + require.NotEmpty(t, rtl.Items) + require.NotNil(t, rtl.Items[0].Sourceable) assert.NotEmpty(t, rtl.Items[0].Sourceable.Name) }) diff --git a/ssh_key.go b/ssh_key.go index d2faea2d4..f4d248bea 100644 --- a/ssh_key.go +++ b/ssh_key.go @@ -84,13 +84,13 @@ func (s *sshKeys) List(ctx context.Context, organization string, options *SSHKey } u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } kl := &SSHKeyList{} - err = s.client.do(ctx, req, kl) + err = req.Do(ctx, kl) if err != nil { return nil, err } @@ -109,13 +109,13 @@ func (s *sshKeys) Create(ctx context.Context, organization string, options SSHKe } u := fmt.Sprintf("organizations/%s/ssh-keys", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } k := &SSHKey{} - err = s.client.do(ctx, req, k) + err = req.Do(ctx, k) if err != nil { return nil, err } @@ -130,13 +130,13 @@ func (s *sshKeys) Read(ctx context.Context, sshKeyID string) (*SSHKey, error) { } u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } k := &SSHKey{} - err = s.client.do(ctx, req, k) + err = req.Do(ctx, k) if err != nil { return nil, err } @@ -151,13 +151,13 @@ func (s *sshKeys) Update(ctx context.Context, sshKeyID string, options SSHKeyUpd } u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } k := &SSHKey{} - err = s.client.do(ctx, req, k) + err = req.Do(ctx, k) if err != nil { return nil, err } @@ -172,12 +172,12 @@ func (s *sshKeys) Delete(ctx context.Context, sshKeyID string) error { } u := fmt.Sprintf("ssh-keys/%s", url.QueryEscape(sshKeyID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o SSHKeyCreateOptions) valid() error { diff --git a/state_version.go b/state_version.go index 225e0a3e0..39173aafb 100644 --- a/state_version.go +++ b/state_version.go @@ -139,13 +139,13 @@ type StateVersionCreateOptions struct { // Optional: Specifies the run to associate the state with. Run *Run `jsonapi:"relation,run,omitempty"` - // Optional: The external, json representation of state data, base64 encoded. - // https://www.terraform.io/internals/json-format#state-representation - // Supplying this state representation can provide more details to the platform + // Optional: The external, json representation of state outputs, base64 encoded. Supplying this field + // will provide more detailed output type information to TFE. + // For more information on the contents of this field: https://www.terraform.io/internals/json-format#values-representation // about the current terraform state. // // **Note**: This field is in BETA, subject to change and not widely available yet. - JSONState *string `jsonapi:"attr,json-state,omitempty"` + JSONStateOutputs *string `jsonapi:"attr,json-state-outputs,omitempty"` } // List all the state versions for a given workspace. @@ -154,13 +154,13 @@ func (s *stateVersions) List(ctx context.Context, options *StateVersionListOptio return nil, err } - req, err := s.client.newRequest("GET", "state-versions", options) + req, err := s.client.NewRequest("GET", "state-versions", options) if err != nil { return nil, err } svl := &StateVersionList{} - err = s.client.do(ctx, req, svl) + err = req.Do(ctx, svl) if err != nil { return nil, err } @@ -178,13 +178,13 @@ func (s *stateVersions) Create(ctx context.Context, workspaceID string, options } u := fmt.Sprintf("workspaces/%s/state-versions", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } sv := &StateVersion{} - err = s.client.do(ctx, req, sv) + err = req.Do(ctx, sv) if err != nil { return nil, err } @@ -202,13 +202,13 @@ func (s *stateVersions) ReadWithOptions(ctx context.Context, svID string, option } u := fmt.Sprintf("state-versions/%s", url.QueryEscape(svID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } sv := &StateVersion{} - err = s.client.do(ctx, req, sv) + err = req.Do(ctx, sv) if err != nil { return nil, err } @@ -231,13 +231,13 @@ func (s *stateVersions) ReadCurrentWithOptions(ctx context.Context, workspaceID } u := fmt.Sprintf("workspaces/%s/current-state-version", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } sv := &StateVersion{} - err = s.client.do(ctx, req, sv) + err = req.Do(ctx, sv) if err != nil { return nil, err } @@ -252,14 +252,14 @@ func (s *stateVersions) ReadCurrent(ctx context.Context, workspaceID string) (*S // Download retrieves the actual stored state of a state version func (s *stateVersions) Download(ctx context.Context, u string) ([]byte, error) { - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") var buf bytes.Buffer - err = s.client.do(ctx, req, &buf) + err = req.Do(ctx, &buf) if err != nil { return nil, err } @@ -274,13 +274,13 @@ func (s *stateVersions) ListOutputs(ctx context.Context, svID string, options *S } u := fmt.Sprintf("state-versions/%s/outputs", url.QueryEscape(svID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } sv := &StateVersionOutputsList{} - err = s.client.do(ctx, req, sv) + err = req.Do(ctx, sv) if err != nil { return nil, err } diff --git a/state_version_integration_test.go b/state_version_integration_test.go index de4166999..37cf688ae 100644 --- a/state_version_integration_test.go +++ b/state_version_integration_test.go @@ -44,6 +44,7 @@ func TestStateVersionsList(t *testing.T) { svl, err := client.StateVersions.List(ctx, options) require.NoError(t, err) + require.NotEmpty(t, svl.Items) // We need to strip the upload URL as that is a dynamic link. svTest1.DownloadURL = "" @@ -121,7 +122,7 @@ func TestStateVersionsCreate(t *testing.T) { t.Fatal(err) } - jsonState, err := ioutil.ReadFile("test-fixtures/ext-state-version/state.json") + jsonStateOutputs, err := ioutil.ReadFile("test-fixtures/json-state-outputs/everything.json") if err != nil { t.Fatal(err) } @@ -169,11 +170,11 @@ func TestStateVersionsCreate(t *testing.T) { } sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{ - Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"), - MD5: String(fmt.Sprintf("%x", md5.Sum(state))), - Serial: Int64(1), - State: String(base64.StdEncoding.EncodeToString(state)), - JSONState: String(base64.StdEncoding.EncodeToString(jsonState)), + Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"), + MD5: String(fmt.Sprintf("%x", md5.Sum(state))), + Serial: Int64(1), + State: String(base64.StdEncoding.EncodeToString(state)), + JSONStateOutputs: String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), }) require.NoError(t, err) @@ -186,6 +187,8 @@ func TestStateVersionsCreate(t *testing.T) { t.Fatal(err) } + // TODO: check state outputs for the ones we sent in JSONStateOutputs + for _, item := range []*StateVersion{ sv, refreshed, diff --git a/state_version_output.go b/state_version_output.go index e6a86cbed..47ac71335 100644 --- a/state_version_output.go +++ b/state_version_output.go @@ -40,13 +40,13 @@ func (s *stateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID strin } u := fmt.Sprintf("workspaces/%s/current-state-version-outputs", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } so := &StateVersionOutputsList{} - err = s.client.do(ctx, req, so) + err = req.Do(ctx, so) if err != nil { return nil, err } @@ -61,13 +61,13 @@ func (s *stateVersionOutputs) Read(ctx context.Context, outputID string) (*State } u := fmt.Sprintf("state-version-outputs/%s", url.QueryEscape(outputID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } so := &StateVersionOutput{} - err = s.client.do(ctx, req, so) + err = req.Do(ctx, so) if err != nil { return nil, err } diff --git a/state_version_output_integration_test.go b/state_version_output_integration_test.go index d1ef12b96..4a2ad8fd2 100644 --- a/state_version_output_integration_test.go +++ b/state_version_output_integration_test.go @@ -33,6 +33,9 @@ func TestStateVersionOutputsRead(t *testing.T) { t.Fatal(err) } + require.NotEmpty(t, sv.Outputs) + require.NotNil(t, sv.Outputs[0]) + output := sv.Outputs[0] t.Run("Read by ID", func(t *testing.T) { @@ -54,17 +57,14 @@ func TestStateVersionOutputsRead(t *testing.T) { t.Run("Read current workspace outputs", func(t *testing.T) { so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID) - - assert.Nil(t, err) - assert.NotNil(t, so) - - assert.Greater(t, len(so.Items), 0, "workspace state version outputs were empty") + require.NoError(t, err) + assert.NotEmpty(t, so.Items) }) t.Run("Sensitive secrets are null", func(t *testing.T) { so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID) - assert.Nil(t, err) - assert.NotNil(t, so) + require.NoError(t, err) + require.NotEmpty(t, so.Items) var found *StateVersionOutput = nil for _, s := range so.Items { diff --git a/task_result.go b/task_result.go index 4abc36706..b7676b6db 100644 --- a/task_result.go +++ b/task_result.go @@ -75,13 +75,13 @@ func (t *taskResults) Read(ctx context.Context, taskResultID string) (*TaskResul } u := fmt.Sprintf("task-results/%s", taskResultID) - req, err := t.client.newRequest("GET", u, nil) + req, err := t.client.NewRequest("GET", u, nil) if err != nil { return nil, err } r := &TaskResult{} - err = t.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } diff --git a/task_stages.go b/task_stages.go index d6699a452..4c659deb9 100644 --- a/task_stages.go +++ b/task_stages.go @@ -85,13 +85,13 @@ func (s *taskStages) Read(ctx context.Context, taskStageID string, options *Task } u := fmt.Sprintf("task-stages/%s", taskStageID) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } t := &TaskStage{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -106,14 +106,14 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL } u := fmt.Sprintf("runs/%s/task-stages", runID) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } tlist := &TaskStageList{} - err = s.client.do(ctx, req, tlist) + err = req.Do(ctx, tlist) if err != nil { return nil, err } diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index 1e838652d..8980c9a65 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -36,6 +36,8 @@ func TestTaskStagesRead(t *testing.T) { Include: []RunIncludeOpt{RunTaskStages}, }) require.NoError(t, err) + require.NotEmpty(t, r.TaskStages) + require.NotNil(t, r.TaskStages[0]) t.Run("without read options", func(t *testing.T) { taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, nil) @@ -62,6 +64,8 @@ func TestTaskStagesRead(t *testing.T) { Include: []TaskStageIncludeOpt{TaskStageTaskResults}, }) require.NoError(t, err) + require.NotEmpty(t, taskStage.TaskResults) + require.NotNil(t, taskStage.TaskResults[0]) t.Run("task results are properly decoded", func(t *testing.T) { assert.NotEmpty(t, taskStage.TaskResults[0].ID) @@ -104,7 +108,7 @@ func TestTaskStagesList(t *testing.T) { taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) require.NoError(t, err) - assert.NotNil(t, taskStageList.Items) + require.NotEmpty(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) assert.Equal(t, 2, len(taskStageList.Items[0].TaskResults)) }) diff --git a/team.go b/team.go index a9c08244a..1e31380a9 100644 --- a/team.go +++ b/team.go @@ -155,13 +155,13 @@ func (s *teams) List(ctx context.Context, organization string, options *TeamList return nil, err } u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } tl := &TeamList{} - err = s.client.do(ctx, req, tl) + err = req.Do(ctx, tl) if err != nil { return nil, err } @@ -179,13 +179,13 @@ func (s *teams) Create(ctx context.Context, organization string, options TeamCre } u := fmt.Sprintf("organizations/%s/teams", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } t := &Team{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -200,13 +200,13 @@ func (s *teams) Read(ctx context.Context, teamID string) (*Team, error) { } u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } t := &Team{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -221,13 +221,13 @@ func (s *teams) Update(ctx context.Context, teamID string, options TeamUpdateOpt } u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } t := &Team{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -242,12 +242,12 @@ func (s *teams) Delete(ctx context.Context, teamID string) error { } u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o TeamCreateOptions) valid() error { diff --git a/team_access.go b/team_access.go index 0f56b3ebe..d1c380923 100644 --- a/team_access.go +++ b/team_access.go @@ -165,13 +165,13 @@ func (s *teamAccesses) List(ctx context.Context, options *TeamAccessListOptions) return nil, err } - req, err := s.client.newRequest("GET", "team-workspaces", options) + req, err := s.client.NewRequest("GET", "team-workspaces", options) if err != nil { return nil, err } tal := &TeamAccessList{} - err = s.client.do(ctx, req, tal) + err = req.Do(ctx, tal) if err != nil { return nil, err } @@ -185,13 +185,13 @@ func (s *teamAccesses) Add(ctx context.Context, options TeamAccessAddOptions) (* return nil, err } - req, err := s.client.newRequest("POST", "team-workspaces", &options) + req, err := s.client.NewRequest("POST", "team-workspaces", &options) if err != nil { return nil, err } ta := &TeamAccess{} - err = s.client.do(ctx, req, ta) + err = req.Do(ctx, ta) if err != nil { return nil, err } @@ -206,13 +206,13 @@ func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAcce } u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } ta := &TeamAccess{} - err = s.client.do(ctx, req, ta) + err = req.Do(ctx, ta) if err != nil { return nil, err } @@ -227,13 +227,13 @@ func (s *teamAccesses) Update(ctx context.Context, teamAccessID string, options } u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } ta := &TeamAccess{} - err = s.client.do(ctx, req, ta) + err = req.Do(ctx, ta) if err != nil { return nil, err } @@ -248,12 +248,12 @@ func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error { } u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *TeamAccessListOptions) valid() error { diff --git a/team_member.go b/team_member.go index c52c60161..06c6eba1f 100644 --- a/team_member.go +++ b/team_member.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/url" - - retryablehttp "github.com/hashicorp/go-retryablehttp" ) // Compile-time proof of interface implementation. @@ -80,13 +78,13 @@ func (s *teamMembers) ListUsers(ctx context.Context, teamID string) ([]*User, er } u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } t := &Team{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -107,13 +105,13 @@ func (s *teamMembers) ListOrganizationMemberships(ctx context.Context, teamID st } u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } t := &Team{} - err = s.client.do(ctx, req, t) + err = req.Do(ctx, t) if err != nil { return nil, err } @@ -133,7 +131,7 @@ func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMember usersOrMemberships := options.kind() u := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships) - var req *retryablehttp.Request + var req *ClientRequest if usersOrMemberships == "users" { var err error @@ -141,7 +139,7 @@ func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMember for _, name := range options.Usernames { members = append(members, &teamMemberUser{Username: name}) } - req, err = s.client.newRequest("POST", u, members) + req, err = s.client.NewRequest("POST", u, members) if err != nil { return err } @@ -151,13 +149,13 @@ func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMember for _, ID := range options.OrganizationMembershipIDs { members = append(members, &teamMemberOrgMembership{ID: ID}) } - req, err = s.client.newRequest("POST", u, members) + req, err = s.client.NewRequest("POST", u, members) if err != nil { return err } } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Remove multiple users from a team. @@ -172,7 +170,7 @@ func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMem usersOrMemberships := options.kind() u := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships) - var req *retryablehttp.Request + var req *ClientRequest if usersOrMemberships == "users" { var err error @@ -180,7 +178,7 @@ func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMem for _, name := range options.Usernames { members = append(members, &teamMemberUser{Username: name}) } - req, err = s.client.newRequest("DELETE", u, members) + req, err = s.client.NewRequest("DELETE", u, members) if err != nil { return err } @@ -190,13 +188,13 @@ func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMem for _, ID := range options.OrganizationMembershipIDs { members = append(members, &teamMemberOrgMembership{ID: ID}) } - req, err = s.client.newRequest("DELETE", u, members) + req, err = s.client.NewRequest("DELETE", u, members) if err != nil { return err } } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // kind returns "users" or "organization-memberships" diff --git a/team_member_integration_test.go b/team_member_integration_test.go index ffa66332e..1f1774a25 100644 --- a/team_member_integration_test.go +++ b/team_member_integration_test.go @@ -240,7 +240,7 @@ func TestTeamMembersRemoveByUsernames(t *testing.T) { } err := client.TeamMembers.Remove(ctx, tmTest.ID, options) - assert.NoError(t, err) + require.NoError(t, err) }) } @@ -271,6 +271,6 @@ func TestTeamMembersRemoveByOrganizationMemberships(t *testing.T) { } err := client.TeamMembers.Remove(ctx, tmTest.ID, options) - assert.NoError(t, err) + require.NoError(t, err) }) } diff --git a/team_token.go b/team_token.go index d299f93c8..e350b1119 100644 --- a/team_token.go +++ b/team_token.go @@ -47,13 +47,13 @@ func (s *teamTokens) Create(ctx context.Context, teamID string) (*TeamToken, err } u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } tt := &TeamToken{} - err = s.client.do(ctx, req, tt) + err = req.Do(ctx, tt) if err != nil { return nil, err } @@ -68,13 +68,13 @@ func (s *teamTokens) Read(ctx context.Context, teamID string) (*TeamToken, error } u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } tt := &TeamToken{} - err = s.client.do(ctx, req, tt) + err = req.Do(ctx, tt) if err != nil { return nil, err } @@ -89,10 +89,10 @@ func (s *teamTokens) Delete(ctx context.Context, teamID string) error { } u := fmt.Sprintf("teams/%s/authentication-token", url.QueryEscape(teamID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/team_token_integration_test.go b/team_token_integration_test.go index 635960a02..14d119efb 100644 --- a/team_token_integration_test.go +++ b/team_token_integration_test.go @@ -54,7 +54,7 @@ func TestTeamTokensRead(t *testing.T) { _, ttTestCleanup := createTeamToken(t, client, tmTest) tt, err := client.TeamTokens.Read(ctx, tmTest.ID) - assert.NoError(t, err) + require.NoError(t, err) assert.NotEmpty(t, tt) ttTestCleanup() @@ -86,7 +86,7 @@ func TestTeamTokensDelete(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.TeamTokens.Delete(ctx, tmTest.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("when a token does not exist", func(t *testing.T) { diff --git a/test-fixtures/ext-state-version/state.json b/test-fixtures/ext-state-version/state.json deleted file mode 100644 index 5acced8cc..000000000 --- a/test-fixtures/ext-state-version/state.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "format_version": "1.0", - "terraform_version": "1.2.0", - "values": { - "outputs": { - "a-decimal": { - "sensitive": false, - "value": 1000.1, - "type": "number" - }, - "a-false-bool": { - "sensitive": false, - "value": false, - "type": "bool" - }, - "a-list": { - "sensitive": false, - "value": [ - "example", - "1001", - "1000.1" - ], - "type": [ - "list", - "string" - ] - }, - "a-long-string": { - "sensitive": false, - "value": "The private integer of the main server instance is where you want to go when you have the most fun in every Terraform instance you can see in the world that you live in except for dogs because they don't run servers in the same place that humans do.", - "type": "string" - }, - "a-object": { - "sensitive": false, - "value": { - "bar": 1000.1, - "example": 1001 - }, - "type": [ - "object", - { - "bar": "number", - "example": "number" - } - ] - }, - "a-sensitive-value": { - "sensitive": true, - "value": "hopefully you cannot see me", - "type": "string" - }, - "a-string": { - "sensitive": false, - "value": "example string", - "type": "string" - }, - "a-true-bool": { - "sensitive": false, - "value": true, - "type": "bool" - }, - "a-tuple": { - "sensitive": false, - "value": [ - 1, - "example" - ], - "type": [ - "tuple", - [ - "number", - "string" - ] - ] - }, - "an-int": { - "sensitive": false, - "value": 1001, - "type": "number" - }, - "escapes": { - "sensitive": false, - "value": "line 1\nline 2\n\\\\\\\\\n", - "type": "string" - }, - "myoutput": { - "sensitive": false, - "value": { - "nesting1": { - "nesting2": { - "nesting3": "4263891374290101092" - } - } - }, - "type": [ - "object", - { - "nesting1": [ - "object", - { - "nesting2": [ - "object", - { - "nesting3": "string" - } - ] - } - ] - } - ] - }, - "random": { - "sensitive": false, - "value": "8b3086889a9ef7a5", - "type": "string" - } - }, - "root_module": { - "resources": [ - { - "address": "null_resource.test", - "mode": "managed", - "type": "null_resource", - "name": "test", - "provider_name": "registry.terraform.io/hashicorp/null", - "schema_version": 0, - "values": { - "id": "4263891374290101092", - "triggers": { - "hello": "wat3" - } - }, - "sensitive_values": { - "triggers": {} - } - }, - { - "address": "random_id.random", - "mode": "managed", - "type": "random_id", - "name": "random", - "provider_name": "registry.terraform.io/hashicorp/random", - "schema_version": 0, - "values": { - "b64_std": "izCGiJqe96U=", - "b64_url": "izCGiJqe96U", - "byte_length": 8, - "dec": "10029664291421878181", - "hex": "8b3086889a9ef7a5", - "id": "izCGiJqe96U", - "keepers": { - "uuid": "437a1415-932b-9f74-c214-184d88215353" - }, - "prefix": null - }, - "sensitive_values": { - "keepers": {} - } - } - ] - } - } -} diff --git a/test-fixtures/json-state-outputs/everything.json b/test-fixtures/json-state-outputs/everything.json new file mode 100644 index 000000000..66c5b45b6 --- /dev/null +++ b/test-fixtures/json-state-outputs/everything.json @@ -0,0 +1,113 @@ +{ + "a-decimal": { + "sensitive": false, + "value": 1000.1, + "type": "number" + }, + "a-false-bool": { + "sensitive": false, + "value": false, + "type": "bool" + }, + "a-list": { + "sensitive": false, + "value": [ + "example", + "1001", + "1000.1" + ], + "type": [ + "list", + "string" + ] + }, + "a-long-string": { + "sensitive": false, + "value": "The private integer of the main server instance is where you want to go when you have the most fun in every Terraform instance you can see in the world that you live in except for dogs because they don't run servers in the same place that humans do.", + "type": "string" + }, + "a-object": { + "sensitive": false, + "value": { + "bar": 1000.1, + "example": 1001 + }, + "type": [ + "object", + { + "bar": "number", + "example": "number" + } + ] + }, + "a-sensitive-value": { + "sensitive": true, + "value": "hopefully you cannot see me", + "type": "string" + }, + "a-string": { + "sensitive": false, + "value": "example string", + "type": "string" + }, + "a-true-bool": { + "sensitive": false, + "value": true, + "type": "bool" + }, + "a-tuple": { + "sensitive": false, + "value": [ + 1, + "example" + ], + "type": [ + "tuple", + [ + "number", + "string" + ] + ] + }, + "an-int": { + "sensitive": false, + "value": 1001, + "type": "number" + }, + "escapes": { + "sensitive": false, + "value": "line 1\nline 2\n\\\\\\\\\n", + "type": "string" + }, + "myoutput": { + "sensitive": false, + "value": { + "nesting1": { + "nesting2": { + "nesting3": "4263891374290101092" + } + } + }, + "type": [ + "object", + { + "nesting1": [ + "object", + { + "nesting2": [ + "object", + { + "nesting3": "string" + } + ] + } + ] + } + ] + }, + "random": { + "sensitive": false, + "value": "8b3086889a9ef7a5", + "type": "string" + } +} diff --git a/testing.go b/testing.go index 66485fb56..501b99f1a 100644 --- a/testing.go +++ b/testing.go @@ -22,13 +22,13 @@ type TestAccountDetails struct { // address associated with the token used to run the tests. func FetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails { tad := &TestAccountDetails{} - req, err := client.newRequest("GET", "account/details", nil) + req, err := client.NewRequest("GET", "account/details", nil) if err != nil { t.Fatalf("could not create account details request: %v", err) } ctx := context.Background() - err = client.do(ctx, req, tad) + err = req.Do(ctx, tad) if err != nil { t.Fatalf("could not fetch test user details: %v", err) } diff --git a/tfe.go b/tfe.go index 953ac6bfc..3a3eb59be 100644 --- a/tfe.go +++ b/tfe.go @@ -117,6 +117,7 @@ type Client struct { remoteAPIVersion string Admin Admin + Agents Agents AgentPools AgentPools AgentTokens AgentTokens Applies Applies @@ -183,6 +184,75 @@ type Meta struct { IPRanges IPRanges } +func (c *Client) NewRequest(method, path string, reqAttr interface{}) (*ClientRequest, error) { + var u *url.URL + var err error + if strings.Contains(path, "/api/registry/") { + u, err = c.registryBaseURL.Parse(path) + if err != nil { + return nil, err + } + } else { + u, err = c.baseURL.Parse(path) + if err != nil { + return nil, err + } + } + + // Create a request specific headers map. + reqHeaders := make(http.Header) + reqHeaders.Set("Authorization", "Bearer "+c.token) + + var body interface{} + switch method { + case "GET": + reqHeaders.Set("Accept", "application/vnd.api+json") + + if reqAttr != nil { + q, err := query.Values(reqAttr) + if err != nil { + return nil, err + } + u.RawQuery = encodeQueryParams(q) + } + case "DELETE", "PATCH", "POST": + reqHeaders.Set("Accept", "application/vnd.api+json") + reqHeaders.Set("Content-Type", "application/vnd.api+json") + + if reqAttr != nil { + if body, err = serializeRequestBody(reqAttr); err != nil { + return nil, err + } + } + case "PUT": + reqHeaders.Set("Accept", "application/json") + reqHeaders.Set("Content-Type", "application/octet-stream") + body = reqAttr + } + + req, err := retryablehttp.NewRequest(method, u.String(), body) + if err != nil { + return nil, err + } + + // Set the default headers. + for k, v := range c.headers { + req.Header[k] = v + } + + // Set the request specific headers. + for k, v := range reqHeaders { + req.Header[k] = v + } + + return &ClientRequest{ + retryableRequest: req, + http: c.http, + limiter: c.limiter, + Header: req.Header, + }, nil +} + // NewClient creates a new Terraform Enterprise API client. func NewClient(cfg *Config) (*Client, error) { config := DefaultConfig() @@ -282,6 +352,7 @@ func NewClient(cfg *Config) (*Client, error) { } // Create the services. + client.Agents = &agents{client: client} client.AgentPools = &agentPools{client: client} client.AgentTokens = &agentTokens{client: client} client.Applies = &applies{client: client} @@ -508,82 +579,8 @@ func (c *Client) configureLimiter(rawLimit string) { c.limiter = rate.NewLimiter(limit, burst) } -// newRequest creates an API request with proper headers and serialization. -// -// A relative URL path can be provided, in which case it is resolved relative to the baseURL -// of the Client. Relative URL paths should always be specified without a preceding slash. Adding a -// preceding slash allows for ignoring the configured baseURL for non-standard endpoints. -// -// If v is supplied, the value will be JSONAPI encoded and included as the -// request body. If the method is GET, the value will be parsed and added as -// query parameters. -func (c *Client) newRequest(method, path string, v interface{}) (*retryablehttp.Request, error) { - var u *url.URL - var err error - if strings.Contains(path, "/api/registry/") { - u, err = c.registryBaseURL.Parse(path) - if err != nil { - return nil, err - } - } else { - u, err = c.baseURL.Parse(path) - if err != nil { - return nil, err - } - } - - // Create a request specific headers map. - reqHeaders := make(http.Header) - reqHeaders.Set("Authorization", "Bearer "+c.token) - - var body interface{} - switch method { - case "GET": - reqHeaders.Set("Accept", "application/vnd.api+json") - - if v != nil { - q, err := query.Values(v) - if err != nil { - return nil, err - } - u.RawQuery = encodeQueryParams(q) - } - case "DELETE", "PATCH", "POST": - reqHeaders.Set("Accept", "application/vnd.api+json") - reqHeaders.Set("Content-Type", "application/vnd.api+json") - - if v != nil { - if body, err = serializeRequestBody(v); err != nil { - return nil, err - } - } - case "PUT": - reqHeaders.Set("Accept", "application/json") - reqHeaders.Set("Content-Type", "application/octet-stream") - body = v - } - - req, err := retryablehttp.NewRequest(method, u.String(), body) - if err != nil { - return nil, err - } - - // Set the default headers. - for k, v := range c.headers { - req.Header[k] = v - } - - // Set the request specific headers. - for k, v := range reqHeaders { - req.Header[k] = v - } - - return req, nil -} - -// Encode encodes the values into ``URL encoded'' form +// encodeQueryParams encodes the values into ``URL encoded'' form // ("bar=baz&foo=quux") sorted by key. - func encodeQueryParams(v url.Values) string { if v == nil { return "" @@ -615,10 +612,9 @@ func encodeQueryParams(v url.Values) string { return buf.String() } -// Helper method that serializes the given ptr or ptr slice into a JSON +// serializeRequestBody serializes the given ptr or ptr slice into a JSON // request. It automatically uses jsonapi or json serialization, depending // on the body type's tags. - func serializeRequestBody(v interface{}) (interface{}, error) { // The body can be a slice of pointers or a pointer. In either // case we want to choose the serialization type based on the @@ -673,99 +669,6 @@ func serializeRequestBody(v interface{}) (interface{}, error) { return buf, nil } -// do sends an API request and returns the API response. The API response -// is JSONAPI decoded and the document's primary data is stored in the value -// pointed to by v, or returned as an error if an API error has occurred. - -// If v implements the io.Writer interface, the raw response body will be -// written to v, without attempting to first decode it. -// -// The provided ctx must be non-nil. If it is canceled or times out, ctx.Err() -// will be returned. - -func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface{}) error { - // Wait will block until the limiter can obtain a new token - // or returns an error if the given context is canceled. - if err := c.limiter.Wait(ctx); err != nil { - return err - } - - // Add the context to the request. - reqWithCxt := req.WithContext(ctx) - - // Execute the request and check the response. - resp, err := c.http.Do(reqWithCxt) - if err != nil { - // If we got an error, and the context has been canceled, - // the context's error is probably more useful. - select { - case <-ctx.Done(): - return ctx.Err() - default: - return err - } - } - defer resp.Body.Close() - - // Basic response checking. - if err := checkResponseCode(resp); err != nil { - return err - } - - // Return here if decoding the response isn't needed. - if v == nil { - return nil - } - - // If v implements io.Writer, write the raw response body. - if w, ok := v.(io.Writer); ok { - _, err := io.Copy(w, resp.Body) - return err - } - - return unmarshalResponse(resp.Body, v) -} - -// customDo is similar to func (c *Client) do(ctx context.Context, req *retryablehttp.Request, v interface{}) error. Except that The IP ranges API is not returning jsonapi like every other endpoint -// which means we need to handle it differently. - -func (i *ipRanges) customDo(ctx context.Context, req *retryablehttp.Request, ir *IPRange) error { - // Wait will block until the limiter can obtain a new token - // or returns an error if the given context is canceled. - if err := i.client.limiter.Wait(ctx); err != nil { - return err - } - - // Add the context to the request. - req = req.WithContext(ctx) - - // Execute the request and check the response. - resp, err := i.client.http.Do(req) - if err != nil { - // If we got an error, and the context has been canceled, - // the context's error is probably more useful. - select { - case <-ctx.Done(): - return ctx.Err() - default: - return err - } - } - defer resp.Body.Close() - - if resp.StatusCode < 200 && resp.StatusCode >= 400 { - return fmt.Errorf("error HTTP response while retrieving IP ranges: %d", resp.StatusCode) - } else if resp.StatusCode == 304 { - return nil - } - - err = json.NewDecoder(resp.Body).Decode(ir) - if err != nil { - return err - } - return nil -} - func unmarshalResponse(responseBody io.Reader, model interface{}) error { // Get the value of model so we can test if it's a struct. dst := reflect.Indirect(reflect.ValueOf(model)) diff --git a/tfe_integration_test.go b/tfe_integration_test.go index c57a642f8..b574d66d9 100644 --- a/tfe_integration_test.go +++ b/tfe_integration_test.go @@ -379,15 +379,15 @@ func createRequest(v interface{}) (*retryablehttp.Request, []byte, error) { return nil, nil, err } - request, err := client.newRequest("POST", "/bar", v) + request, err := client.NewRequest("POST", "/bar", v) if err != nil { return nil, nil, err } - body, err := request.BodyBytes() + body, err := request.retryableRequest.BodyBytes() if err != nil { - return request, nil, err + return request.retryableRequest, nil, err } - return request, body, nil + return request.retryableRequest, body, nil } func TestClient_configureLimiter(t *testing.T) { diff --git a/tfe_test.go b/tfe_test.go index eef419463..1fe614cf8 100644 --- a/tfe_test.go +++ b/tfe_test.go @@ -141,10 +141,10 @@ func Test_RegistryBasePath(t *testing.T) { t.Run("ensures client creates a request with registry base path", func(t *testing.T) { path := "/api/registry/some/path/to/resource" - req, err := client.newRequest("GET", path, nil) + req, err := client.NewRequest("GET", path, nil) require.NoError(t, err) expected := os.Getenv("TFE_ADDRESS") + path - assert.Equal(t, req.URL.String(), expected) + assert.Equal(t, req.retryableRequest.URL.String(), expected) }) } diff --git a/user.go b/user.go index 0795f5032..f7b8f5b3c 100644 --- a/user.go +++ b/user.go @@ -62,13 +62,13 @@ type UserUpdateOptions struct { // ReadCurrent reads the details of the currently authenticated user. func (s *users) ReadCurrent(ctx context.Context) (*User, error) { - req, err := s.client.newRequest("GET", "account/details", nil) + req, err := s.client.NewRequest("GET", "account/details", nil) if err != nil { return nil, err } u := &User{} - err = s.client.do(ctx, req, u) + err = req.Do(ctx, u) if err != nil { return nil, err } @@ -78,13 +78,13 @@ func (s *users) ReadCurrent(ctx context.Context) (*User, error) { // UpdateCurrent updates attributes of the currently authenticated user. func (s *users) UpdateCurrent(ctx context.Context, options UserUpdateOptions) (*User, error) { - req, err := s.client.newRequest("PATCH", "account/update", &options) + req, err := s.client.NewRequest("PATCH", "account/update", &options) if err != nil { return nil, err } u := &User{} - err = s.client.do(ctx, req, u) + err = req.Do(ctx, u) if err != nil { return nil, err } diff --git a/user_integration_test.go b/user_integration_test.go index 3aef6c7b3..7d965e09d 100644 --- a/user_integration_test.go +++ b/user_integration_test.go @@ -16,7 +16,8 @@ func TestUsersReadCurrent(t *testing.T) { ctx := context.Background() u, err := client.Users.ReadCurrent(ctx) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotEmpty(t, u.ID) assert.NotEmpty(t, u.AvatarURL) assert.NotEmpty(t, u.Username) @@ -49,7 +50,7 @@ func TestUsersUpdate(t *testing.T) { require.NoError(t, err) u, err := client.Users.ReadCurrent(ctx) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, u, uTest) }) @@ -60,7 +61,7 @@ func TestUsersUpdate(t *testing.T) { require.NoError(t, err) u, err := client.Users.ReadCurrent(ctx) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "NewTestUsername", u.Username) }) @@ -81,7 +82,7 @@ func TestUsersUpdate(t *testing.T) { t.Fatalf("cannot test with user %q because both email and unconfirmed email are empty", u.ID) } - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "newtestemail@hashicorp.com", email) }) diff --git a/user_token.go b/user_token.go index 56de5b859..d27feacd4 100644 --- a/user_token.go +++ b/user_token.go @@ -62,13 +62,13 @@ func (s *userTokens) Create(ctx context.Context, userID string, options UserToke } u := fmt.Sprintf("users/%s/authentication-tokens", url.QueryEscape(userID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } ut := &UserToken{} - err = s.client.do(ctx, req, ut) + err = req.Do(ctx, ut) if err != nil { return nil, err } @@ -83,13 +83,13 @@ func (s *userTokens) List(ctx context.Context, userID string) (*UserTokenList, e } u := fmt.Sprintf("users/%s/authentication-tokens", url.QueryEscape(userID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } tl := &UserTokenList{} - err = s.client.do(ctx, req, tl) + err = req.Do(ctx, tl) if err != nil { return nil, err } @@ -104,13 +104,13 @@ func (s *userTokens) Read(ctx context.Context, tokenID string) (*UserToken, erro } u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(tokenID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } tt := &UserToken{} - err = s.client.do(ctx, req, tt) + err = req.Do(ctx, tt) if err != nil { return nil, err } @@ -125,10 +125,10 @@ func (s *userTokens) Delete(ctx context.Context, tokenID string) error { } u := fmt.Sprintf("authentication-tokens/%s", url.QueryEscape(tokenID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/variable.go b/variable.go index 53ea17458..c18975a8e 100644 --- a/variable.go +++ b/variable.go @@ -131,13 +131,13 @@ func (s *variables) List(ctx context.Context, workspaceID string, options *Varia } u := fmt.Sprintf("workspaces/%s/vars", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } vl := &VariableList{} - err = s.client.do(ctx, req, vl) + err = req.Do(ctx, vl) if err != nil { return nil, err } @@ -155,13 +155,13 @@ func (s *variables) Create(ctx context.Context, workspaceID string, options Vari } u := fmt.Sprintf("workspaces/%s/vars", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } v := &Variable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -179,13 +179,13 @@ func (s *variables) Read(ctx context.Context, workspaceID, variableID string) (* } u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } v := &Variable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -203,13 +203,13 @@ func (s *variables) Update(ctx context.Context, workspaceID, variableID string, } u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } v := &Variable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -227,12 +227,12 @@ func (s *variables) Delete(ctx context.Context, workspaceID, variableID string) } u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o VariableCreateOptions) valid() error { diff --git a/variable_integration_test.go b/variable_integration_test.go index 12ca04bb7..087a323f2 100644 --- a/variable_integration_test.go +++ b/variable_integration_test.go @@ -322,7 +322,7 @@ func TestVariablesDelete(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.Variables.Delete(ctx, wTest.ID, vTest.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with non existing variable ID", func(t *testing.T) { diff --git a/variable_set.go b/variable_set.go index d666dae66..3a9a1f9ec 100644 --- a/variable_set.go +++ b/variable_set.go @@ -165,13 +165,13 @@ func (s *variableSets) List(ctx context.Context, organization string, options *V } u := fmt.Sprintf("organizations/%s/varsets", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } vl := &VariableSetList{} - err = s.client.do(ctx, req, vl) + err = req.Do(ctx, vl) if err != nil { return nil, err } @@ -189,13 +189,13 @@ func (s *variableSets) Create(ctx context.Context, organization string, options } u := fmt.Sprintf("organizations/%s/varsets", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, options) + req, err := s.client.NewRequest("POST", u, options) if err != nil { return nil, err } vl := &VariableSet{} - err = s.client.do(ctx, req, vl) + err = req.Do(ctx, vl) if err != nil { return nil, err } @@ -210,13 +210,13 @@ func (s *variableSets) Read(ctx context.Context, variableSetID string, options * } u := fmt.Sprintf("varsets/%s", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } vs := &VariableSet{} - err = s.client.do(ctx, req, vs) + err = req.Do(ctx, vs) if err != nil { return nil, err } @@ -231,13 +231,13 @@ func (s *variableSets) Update(ctx context.Context, variableSetID string, options } u := fmt.Sprintf("varsets/%s", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("PATCH", u, options) + req, err := s.client.NewRequest("PATCH", u, options) if err != nil { return nil, err } v := &VariableSet{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -252,12 +252,12 @@ func (s *variableSets) Delete(ctx context.Context, variableSetID string) error { } u := fmt.Sprintf("varsets/%s", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Apply variable set to workspaces in the supplied list. @@ -271,12 +271,12 @@ func (s *variableSets) ApplyToWorkspaces(ctx context.Context, variableSetID stri } u := fmt.Sprintf("varsets/%s/relationships/workspaces", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("POST", u, options.Workspaces) + req, err := s.client.NewRequest("POST", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Remove variable set from workspaces in the supplied list. @@ -290,12 +290,12 @@ func (s *variableSets) RemoveFromWorkspaces(ctx context.Context, variableSetID s } u := fmt.Sprintf("varsets/%s/relationships/workspaces", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("DELETE", u, options.Workspaces) + req, err := s.client.NewRequest("DELETE", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // Update variable set to be applied to only the workspaces in the supplied list. @@ -312,13 +312,13 @@ func (s *variableSets) UpdateWorkspaces(ctx context.Context, variableSetID strin // We force inclusion of workspaces as that is the primary data for which we are concerned with confirming changes. u := fmt.Sprintf("varsets/%s?include=%s", url.QueryEscape(variableSetID), VariableSetWorkspaces) - req, err := s.client.newRequest("PATCH", u, &o) + req, err := s.client.NewRequest("PATCH", u, &o) if err != nil { return nil, err } v := &VariableSet{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } diff --git a/variable_set_test.go b/variable_set_test.go index e967b0f85..bab062a72 100644 --- a/variable_set_test.go +++ b/variable_set_test.go @@ -23,6 +23,7 @@ func TestVariableSetsList(t *testing.T) { t.Run("without list options", func(t *testing.T) { vsl, err := client.VariableSets.List(ctx, orgTest.Name, nil) require.NoError(t, err) + require.NotEmpty(t, vsl.Items) assert.Contains(t, vsl.Items, vsTest1) assert.Contains(t, vsl.Items, vsTest2) diff --git a/variable_set_variable.go b/variable_set_variable.go index dea5835ff..5f98399c7 100644 --- a/variable_set_variable.go +++ b/variable_set_variable.go @@ -72,13 +72,13 @@ func (s *variableSetVariables) List(ctx context.Context, variableSetID string, o } u := fmt.Sprintf("varsets/%s/relationships/vars", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } vl := &VariableSetVariableList{} - err = s.client.do(ctx, req, vl) + err = req.Do(ctx, vl) if err != nil { return nil, err } @@ -135,13 +135,13 @@ func (s *variableSetVariables) Create(ctx context.Context, variableSetID string, } u := fmt.Sprintf("varsets/%s/relationships/vars", url.QueryEscape(variableSetID)) - req, err := s.client.newRequest("POST", u, options) + req, err := s.client.NewRequest("POST", u, options) if err != nil { return nil, err } v := &VariableSetVariable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -159,14 +159,14 @@ func (s *variableSetVariables) Read(ctx context.Context, variableSetID, variable } u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.QueryEscape(variableSetID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } v := &VariableSetVariable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -208,13 +208,13 @@ func (s *variableSetVariables) Update(ctx context.Context, variableSetID, variab } u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.QueryEscape(variableSetID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("PATCH", u, options) + req, err := s.client.NewRequest("PATCH", u, options) if err != nil { return nil, err } v := &VariableSetVariable{} - err = s.client.do(ctx, req, v) + err = req.Do(ctx, v) if err != nil { return nil, err } @@ -232,10 +232,10 @@ func (s *variableSetVariables) Delete(ctx context.Context, variableSetID, variab } u := fmt.Sprintf("varsets/%s/relationships/vars/%s", url.QueryEscape(variableSetID), url.QueryEscape(variableID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } diff --git a/variable_set_variable_test.go b/variable_set_variable_test.go index cc2107103..8faa94628 100644 --- a/variable_set_variable_test.go +++ b/variable_set_variable_test.go @@ -2,9 +2,10 @@ package tfe import ( "context" + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) func TestVariableSetVariablesList(t *testing.T) { @@ -334,7 +335,7 @@ func TestVariableSetVariablesDelete(t *testing.T) { t.Run("with valid options", func(t *testing.T) { err := client.VariableSetVariables.Delete(ctx, vsTest.ID, vTest.ID) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("with non existing variable ID", func(t *testing.T) { diff --git a/workspace.go b/workspace.go index eb8f9c9b8..cc8ace07a 100644 --- a/workspace.go +++ b/workspace.go @@ -182,6 +182,7 @@ type VCSRepo struct { OAuthTokenID string `jsonapi:"attr,oauth-token-id"` RepositoryHTTPURL string `jsonapi:"attr,repository-http-url"` ServiceProvider string `jsonapi:"attr,service-provider"` + TagsRegex string `jsonapi:"attr,tags-regex"` WebhookURL string `jsonapi:"attr,webhook-url"` } @@ -357,6 +358,7 @@ type VCSRepoOptions struct { Identifier *string `json:"identifier,omitempty"` IngressSubmodules *bool `json:"ingress-submodules,omitempty"` OAuthTokenID *string `json:"oauth-token-id,omitempty"` + TagsRegex *string `json:"tags-regex,omitempty"` } // WorkspaceUpdateOptions represents the options for updating a workspace. @@ -536,13 +538,13 @@ func (s *workspaces) List(ctx context.Context, organization string, options *Wor } u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } wl := &WorkspaceList{} - err = s.client.do(ctx, req, wl) + err = req.Do(ctx, wl) if err != nil { return nil, err } @@ -560,13 +562,13 @@ func (s *workspaces) Create(ctx context.Context, organization string, options Wo } u := fmt.Sprintf("organizations/%s/workspaces", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -596,13 +598,13 @@ func (s *workspaces) ReadWithOptions(ctx context.Context, organization, workspac url.QueryEscape(organization), url.QueryEscape(workspace), ) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -626,13 +628,13 @@ func (s *workspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string } u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -651,13 +653,13 @@ func (s *workspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, } u := fmt.Sprintf("workspaces/%s?include=readme", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } r := &workspaceWithReadme{} - err = s.client.do(ctx, req, r) + err = req.Do(ctx, r) if err != nil { return nil, err } @@ -685,13 +687,13 @@ func (s *workspaces) Update(ctx context.Context, organization, workspace string, url.QueryEscape(organization), url.QueryEscape(workspace), ) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -706,13 +708,13 @@ func (s *workspaces) UpdateByID(ctx context.Context, workspaceID string, options } u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -734,12 +736,12 @@ func (s *workspaces) Delete(ctx context.Context, organization, workspace string) url.QueryEscape(organization), url.QueryEscape(workspace), ) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // DeleteByID deletes a workspace by its ID. @@ -749,12 +751,12 @@ func (s *workspaces) DeleteByID(ctx context.Context, workspaceID string) error { } u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // RemoveVCSConnection from a workspace. @@ -772,13 +774,13 @@ func (s *workspaces) RemoveVCSConnection(ctx context.Context, organization, work url.QueryEscape(workspace), ) - req, err := s.client.newRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{}) + req, err := s.client.NewRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{}) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -794,13 +796,13 @@ func (s *workspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID st u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{}) + req, err := s.client.NewRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{}) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -815,13 +817,13 @@ func (s *workspaces) Lock(ctx context.Context, workspaceID string, options Works } u := fmt.Sprintf("workspaces/%s/actions/lock", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -836,13 +838,13 @@ func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace } u := fmt.Sprintf("workspaces/%s/actions/unlock", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -857,13 +859,13 @@ func (s *workspaces) ForceUnlock(ctx context.Context, workspaceID string) (*Work } u := fmt.Sprintf("workspaces/%s/actions/force-unlock", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -881,13 +883,13 @@ func (s *workspaces) AssignSSHKey(ctx context.Context, workspaceID string, optio } u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -902,13 +904,13 @@ func (s *workspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*W } u := fmt.Sprintf("workspaces/%s/relationships/ssh-key", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("PATCH", u, &workspaceUnassignSSHKeyOptions{}) + req, err := s.client.NewRequest("PATCH", u, &workspaceUnassignSSHKeyOptions{}) if err != nil { return nil, err } w := &Workspace{} - err = s.client.do(ctx, req, w) + err = req.Do(ctx, w) if err != nil { return nil, err } @@ -924,13 +926,13 @@ func (s *workspaces) ListRemoteStateConsumers(ctx context.Context, workspaceID s u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } wl := &WorkspaceList{} - err = s.client.do(ctx, req, wl) + err = req.Do(ctx, wl) if err != nil { return nil, err } @@ -948,12 +950,12 @@ func (s *workspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID st } u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, options.Workspaces) + req, err := s.client.NewRequest("POST", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // RemoveRemoteStateConsumers removes the remote state consumers for a given workspace. @@ -966,12 +968,12 @@ func (s *workspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID } u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("DELETE", u, options.Workspaces) + req, err := s.client.NewRequest("DELETE", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // UpdateRemoteStateConsumers removes the remote state consumers for a given workspace. @@ -984,12 +986,12 @@ func (s *workspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID } u := fmt.Sprintf("workspaces/%s/relationships/remote-state-consumers", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("PATCH", u, options.Workspaces) + req, err := s.client.NewRequest("PATCH", u, options.Workspaces) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // ListTags returns the tags for a given workspace. @@ -1000,13 +1002,13 @@ func (s *workspaces) ListTags(ctx context.Context, workspaceID string, options * u := fmt.Sprintf("workspaces/%s/relationships/tags", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } tl := &TagList{} - err = s.client.do(ctx, req, tl) + err = req.Do(ctx, tl) if err != nil { return nil, err } @@ -1024,12 +1026,12 @@ func (s *workspaces) AddTags(ctx context.Context, workspaceID string, options Wo } u := fmt.Sprintf("workspaces/%s/relationships/tags", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("POST", u, options.Tags) + req, err := s.client.NewRequest("POST", u, options.Tags) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } // RemoveTags removes a list of tags from a workspace. @@ -1042,12 +1044,12 @@ func (s *workspaces) RemoveTags(ctx context.Context, workspaceID string, options } u := fmt.Sprintf("workspaces/%s/relationships/tags", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("DELETE", u, options.Tags) + req, err := s.client.NewRequest("DELETE", u, options.Tags) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o WorkspaceCreateOptions) valid() error { @@ -1070,6 +1072,18 @@ func (o WorkspaceCreateOptions) valid() error { o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 { return ErrUnsupportedBothTriggerPatternsAndPrefixes } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 { + return ErrUnsupportedBothTagsRegexAndTriggerPatterns + } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 { + return ErrUnsupportedBothTagsRegexAndTriggerPrefixes + } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.FileTriggersEnabled != nil && *o.FileTriggersEnabled { + return ErrUnsupportedBothTagsRegexAndFileTriggersEnabled + } return nil } @@ -1089,6 +1103,19 @@ func (o WorkspaceUpdateOptions) valid() error { return ErrUnsupportedBothTriggerPatternsAndPrefixes } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 { + return ErrUnsupportedBothTagsRegexAndTriggerPatterns + } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 { + return ErrUnsupportedBothTagsRegexAndTriggerPrefixes + } + if o.VCSRepo != nil && o.VCSRepo.TagsRegex != nil && + o.FileTriggersEnabled != nil && *o.FileTriggersEnabled { + return ErrUnsupportedBothTagsRegexAndFileTriggersEnabled + } + return nil } diff --git a/workspace_integration_test.go b/workspace_integration_test.go index 0a9dbf476..6fc05de1f 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -139,31 +139,31 @@ func TestWorkspacesList(t *testing.T) { Include: []WSIncludeOpt{WSOrganization}, }) - assert.NoError(t, err) - - assert.NotEmpty(t, wl.Items) - assert.NotNil(t, wl.Items[0].Organization) + require.NoError(t, err) + require.NotEmpty(t, wl.Items) + require.NotNil(t, wl.Items[0].Organization) assert.NotEmpty(t, wl.Items[0].Organization.Email) }) t.Run("with current-state-version,current-run included", func(t *testing.T) { - _, rCleanup := createAppliedRun(t, client, wTest1) + _, rCleanup := createRunApply(t, client, wTest1) t.Cleanup(rCleanup) wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{ Include: []WSIncludeOpt{WSCurrentStateVer, WSCurrentRun}, }) - assert.NoError(t, err) - assert.NotEmpty(t, wl.Items) + require.NoError(t, err) + require.NotEmpty(t, wl.Items) foundWTest1 := false for _, ws := range wl.Items { if ws.ID == wTest1.ID { foundWTest1 = true - assert.NotNil(t, wl.Items[0].CurrentStateVersion) + require.NotNil(t, wl.Items[0].CurrentStateVersion) assert.NotEmpty(t, wl.Items[0].CurrentStateVersion.DownloadURL) - assert.NotNil(t, wl.Items[0].CurrentRun) + + require.NotNil(t, wl.Items[0].CurrentRun) assert.NotEmpty(t, wl.Items[0].CurrentRun.Message) } } @@ -344,6 +344,76 @@ func TestWorkspacesCreate(t *testing.T) { assert.EqualError(t, err, ErrUnsupportedBothTriggerPatternsAndPrefixes.Error()) }) + t.Run("when options include tags-regex(behind a feature flag)", func(t *testing.T) { + skipIfBeta(t) + // Remove the below organization creation and use the one from the outer scope once the feature flag is removed + orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{ + Name: String("tst-" + randomString(t)[0:20] + "-git-tag-ff-on"), + Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))), + }) + defer orgTestCleanup() + + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + VCSRepo: &VCSRepoOptions{ + TagsRegex: String("barfoo")}, + } + + w, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, options) + defer wTestCleanup() + assert.Equal(t, *options.VCSRepo.TagsRegex, w.VCSRepo.TagsRegex) + + // Get a refreshed view from the API. + refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name) + require.NoError(t, err) + + for _, item := range []*Workspace{ + w, + refreshed, + } { + assert.Equal(t, *options.VCSRepo.TagsRegex, item.VCSRepo.TagsRegex) + } + }) + + t.Run("when options include both non-empty tags-regex and trigger-patterns error is returned", func(t *testing.T) { + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + } + w, err := client.Workspaces.Create(ctx, orgTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPatterns.Error()) + }) + + t.Run("when options include both non-empty tags-regex and trigger-prefixes error is returned", func(t *testing.T) { + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + TriggerPrefixes: []string{"/module-1", "/module-2"}, + } + w, err := client.Workspaces.Create(ctx, orgTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPrefixes.Error()) + }) + + t.Run("when options include both non-empty tags-regex and file-triggers-enabled as true an error is returned", func(t *testing.T) { + options := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + } + w, err := client.Workspaces.Create(ctx, orgTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndFileTriggersEnabled.Error()) + }) + t.Run("when options include trigger-patterns populated and empty trigger-paths workspace is created", func(t *testing.T) { // Remove the below organization creation and use the one from the outer scope once the feature flag is removed orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{ @@ -469,14 +539,27 @@ func TestWorkspacesReadWithHistory(t *testing.T) { wTest, wTestCleanup := createWorkspace(t, client, orgTest) defer wTestCleanup() - _, rCleanup := createAppliedRun(t, client, wTest) + _, rCleanup := createRunApply(t, client, wTest) defer rCleanup() - w, err := client.Workspaces.Read(context.Background(), orgTest.Name, wTest.Name) - require.NoError(t, err) + _, err := retry(func() (interface{}, error) { + w, err := client.Workspaces.Read(context.Background(), orgTest.Name, wTest.Name) + require.NoError(t, err) + + if w.RunsCount != 1 { + return nil, fmt.Errorf("expected %d runs but found %d", 1, w.RunsCount) + } + + if w.ResourceCount != 1 { + return nil, fmt.Errorf("expected %d resources but found %d", 1, w.ResourceCount) + } + + return w, nil + }) - assert.Equal(t, 1, w.RunsCount) - assert.Equal(t, 1, w.ResourceCount) + if err != nil { + t.Error(err) + } } func TestWorkspacesReadReadme(t *testing.T) { @@ -489,7 +572,7 @@ func TestWorkspacesReadReadme(t *testing.T) { wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, WorkspaceCreateOptions{}) defer wTestCleanup() - _, rCleanup := createAppliedRun(t, client, wTest) + _, rCleanup := createRunApply(t, client, wTest) defer rCleanup() t.Run("when the readme exists", func(t *testing.T) { @@ -749,6 +832,89 @@ func TestWorkspacesUpdate(t *testing.T) { assert.Equal(t, options.TriggerPatterns, item.TriggerPatterns) } }) + + t.Run("when options include VCSRepo tags-regex (behind a feature flag)", func(t *testing.T) { + skipIfBeta(t) + // Remove the below organization and workspace creation and use the one from the outer scope once the feature flag is removed + orgTest, orgTestCleanup := createOrganizationWithOptions(t, client, OrganizationCreateOptions{ + Name: String("tst-" + randomString(t)[0:20] + "-git-tag-ff-on"), + Email: String(fmt.Sprintf("%s@tfe.local", randomString(t))), + }) + defer orgTestCleanup() + + createOptions := WorkspaceCreateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + VCSRepo: &VCSRepoOptions{ + TagsRegex: String("barfoo")}, + } + + wTest, wTestCleanup := createWorkspaceWithVCS(t, client, orgTest, createOptions) + defer wTestCleanup() + assert.Equal(t, *createOptions.VCSRepo.TagsRegex, wTest.VCSRepo.TagsRegex) + + assert.Equal(t, wTest.VCSRepo.TagsRegex, *String("barfoo")) // Sanity test + + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + require.NoError(t, err) + + assert.Equal(t, w.VCSRepo.TagsRegex, *String("foobar")) // Sanity test + + // Get a refreshed view from the API. + refreshed, err := client.Workspaces.Read(ctx, orgTest.Name, *options.Name) + require.NoError(t, err) + + for _, item := range []*Workspace{ + w, + refreshed, + } { + assert.Empty(t, options.TriggerPrefixes) + assert.Empty(t, options.TriggerPatterns, item.TriggerPatterns) + } + }) + + t.Run("when options include tags-regex and file-triggers-enabled is true an error is returned", func(t *testing.T) { + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(true), + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndFileTriggersEnabled.Error()) + }) + + t.Run("when options include both tags-regex and trigger-prefixes an error is returned", func(t *testing.T) { + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + TriggerPrefixes: []string{"/module-1", "/module-2"}, + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPrefixes.Error()) + }) + + t.Run("when options include both tags-regex and trigger-patterns error is returned", func(t *testing.T) { + options := WorkspaceUpdateOptions{ + Name: String("foobar"), + FileTriggersEnabled: Bool(false), + TriggerPatterns: []string{"/module-1/**/*", "/**/networking/*"}, + VCSRepo: &VCSRepoOptions{TagsRegex: String("foobar")}, + } + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options) + + assert.Nil(t, w) + assert.EqualError(t, err, ErrUnsupportedBothTagsRegexAndTriggerPatterns.Error()) + }) } func TestWorkspacesUpdateByID(t *testing.T) { diff --git a/workspace_run_task.go b/workspace_run_task.go index cc2eb4cd7..4dad5ecca 100644 --- a/workspace_run_task.go +++ b/workspace_run_task.go @@ -75,13 +75,13 @@ func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, option } u := fmt.Sprintf("workspaces/%s/tasks", url.QueryEscape(workspaceID)) - req, err := s.client.newRequest("GET", u, options) + req, err := s.client.NewRequest("GET", u, options) if err != nil { return nil, err } rl := &WorkspaceRunTaskList{} - err = s.client.do(ctx, req, rl) + err = req.Do(ctx, rl) if err != nil { return nil, err } @@ -104,13 +104,13 @@ func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID, workspaceTask url.QueryEscape(workspaceID), url.QueryEscape(workspaceTaskID), ) - req, err := s.client.newRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err } wr := &WorkspaceRunTask{} - err = s.client.do(ctx, req, wr) + err = req.Do(ctx, wr) if err != nil { return nil, err } @@ -129,13 +129,13 @@ func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, opti } u := fmt.Sprintf("workspaces/%s/tasks", workspaceID) - req, err := s.client.newRequest("POST", u, &options) + req, err := s.client.NewRequest("POST", u, &options) if err != nil { return nil, err } wr := &WorkspaceRunTask{} - err = s.client.do(ctx, req, wr) + err = req.Do(ctx, wr) if err != nil { return nil, err } @@ -158,13 +158,13 @@ func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID, workspaceTa url.QueryEscape(workspaceID), url.QueryEscape(workspaceTaskID), ) - req, err := s.client.newRequest("PATCH", u, &options) + req, err := s.client.NewRequest("PATCH", u, &options) if err != nil { return nil, err } wr := &WorkspaceRunTask{} - err = s.client.do(ctx, req, wr) + err = req.Do(ctx, wr) if err != nil { return nil, err } @@ -187,12 +187,12 @@ func (s *workspaceRunTasks) Delete(ctx context.Context, workspaceID, workspaceTa url.QueryEscape(workspaceID), url.QueryEscape(workspaceTaskID), ) - req, err := s.client.newRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil) if err != nil { return err } - return s.client.do(ctx, req, nil) + return req.Do(ctx, nil) } func (o *WorkspaceRunTaskCreateOptions) valid() error { diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go index 234033588..1b9c54345 100644 --- a/workspace_run_task_integration_test.go +++ b/workspace_run_task_integration_test.go @@ -12,7 +12,7 @@ import ( ) func TestWorkspaceRunTasksCreate(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -33,6 +33,10 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { }) require.NoError(t, err) + defer func() { + client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wr.ID) + }() + assert.NotEmpty(t, wr.ID) assert.Equal(t, wr.EnforcementLevel, Mandatory) @@ -43,7 +47,7 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { } func TestWorkspaceRunTasksList(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -77,7 +81,7 @@ func TestWorkspaceRunTasksList(t *testing.T) { } func TestWorkspaceRunTasksRead(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -112,7 +116,7 @@ func TestWorkspaceRunTasksRead(t *testing.T) { } func TestWorkspaceRunTasksUpdate(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -143,7 +147,7 @@ func TestWorkspaceRunTasksUpdate(t *testing.T) { } func TestWorkspaceRunTasksDelete(t *testing.T) { - skipIfBeta(t) + skipIfFreeOnly(t) client := testClient(t) ctx := context.Background() @@ -173,8 +177,7 @@ func TestWorkspaceRunTasksDelete(t *testing.T) { }) t.Run("when the workspace does not exist", func(t *testing.T) { - wkspaceTestCleanup() - err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID) + err := client.WorkspaceRunTasks.Delete(ctx, "does-not-exist", wrTaskTest.ID) assert.EqualError(t, err, ErrResourceNotFound.Error()) }) }