diff --git a/.github/actions/lint-provider-tfe/action.yml b/.github/actions/lint-provider-tfe/action.yml index 041b1f99f..55c252fa8 100644 --- a/.github/actions/lint-provider-tfe/action.yml +++ b/.github/actions/lint-provider-tfe/action.yml @@ -7,7 +7,7 @@ runs: using: composite steps: - name: Setup Go Environment - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version-file: "go.mod" cache: true diff --git a/.github/actions/test-provider-tfe/action.yml b/.github/actions/test-provider-tfe/action.yml index 76170836d..5f83de881 100644 --- a/.github/actions/test-provider-tfe/action.yml +++ b/.github/actions/test-provider-tfe/action.yml @@ -1,7 +1,7 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 -name: TESTS ARE TEMPOARILY DISABLED +name: Test description: Tests terraform-provider-tfe within a matrix inputs: admin_configuration_token: @@ -51,76 +51,72 @@ inputs: runs: using: composite steps: - - name: NO-OP + - name: Set up Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod + cache: true + + - name: Sync dependencies shell: bash run: | - echo "Tests are skipped. Please test manually." - # - name: Set up Go - # uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 - # with: - # go-version-file: go.mod - # cache: true - - # - name: Sync dependencies - # shell: bash - # run: | - # go mod download - # go mod tidy + go mod download + go mod tidy - # - name: Install gotestsum - # shell: bash - # run: go install gotest.tools/gotestsum@latest + - name: Install gotestsum + shell: bash + run: go install gotest.tools/gotestsum@latest - # - name: Download artifact - # id: download-artifact - # uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0 - # with: - # workflow_conclusion: success - # name: junit-test-summary - # if_no_artifact_found: warn - # branch: main + - name: Download artifact + id: download-artifact + uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0 + with: + workflow_conclusion: success + name: junit-test-summary + if_no_artifact_found: warn + branch: main - # - name: Split acceptance tests - # id: test_split - # uses: hashicorp-forge/go-test-split-action@796beedbdb3d1bea14cad2d3057bab5c5cf15fe5 # v1.0.2 - # with: - # index: ${{ inputs.matrix_index }} - # total: ${{ inputs.matrix_total }} - # junit-summary: ./ci-summary-provider.xml - # # When tests are split and run concurrently, lists_tests arg in ci.yml will skip the TestAccTFESAMLSettings_omnibus test suite - # list: ${{ inputs.list_tests }} + - name: Split acceptance tests + id: test_split + uses: hashicorp-forge/go-test-split-action@796beedbdb3d1bea14cad2d3057bab5c5cf15fe5 # v1.0.2 + with: + index: ${{ inputs.matrix_index }} + total: ${{ inputs.matrix_total }} + junit-summary: ./ci-summary-provider.xml + # When tests are split and run concurrently, lists_tests arg in ci.yml will skip the TestAccTFESAMLSettings_omnibus test suite + list: ${{ inputs.list_tests }} - # - name: Run Tests - # shell: bash - # env: - # TFE_HOSTNAME: "${{ inputs.hostname }}" - # TFE_TOKEN: "${{ inputs.token }}" - # TFE_ADMIN_CONFIGURATION_TOKEN: ${{ inputs.admin_configuration_token }} - # TFE_ADMIN_PROVISION_LICENSES_TOKEN: ${{ inputs.admin_provision_licenses_token }} - # TFE_ADMIN_SECURITY_MAINTENANCE_TOKEN: ${{ inputs.admin_security_maintenance_token }} - # TFE_ADMIN_SITE_ADMIN_TOKEN: ${{ inputs.admin_site_admin_token }} - # TFE_ADMIN_SUBSCRIPTION_TOKEN: ${{ inputs.admin_subscription_token }} - # TFE_ADMIN_SUPPORT_TOKEN: ${{ inputs.admin_support_token }} - # TFE_ADMIN_VERSION_MAINTENANCE_TOKEN: ${{ inputs.admin_version_maintenance_token }} - # TFE_USER1: tfe-provider-user1 - # TFE_USER2: tfe-provider-user2 - # TF_ACC: "1" - # ENABLE_TFE: "${{ inputs.enterprise }}" - # RUN_TASKS_URL: "http://testing-mocks.tfe:22180/runtasks/pass" - # GITHUB_POLICY_SET_IDENTIFIER: "hashicorp/test-policy-set" - # GITHUB_REGISTRY_MODULE_IDENTIFIER: "hashicorp/terraform-random-module" - # GITHUB_WORKSPACE_IDENTIFIER: "hashicorp/terraform-random-module" - # GITHUB_WORKSPACE_BRANCH: "main" - # GITHUB_TOKEN: "${{ inputs.testing-github-token }}" - # MOD_PROVIDER: github.com/hashicorp/terraform-provider-tfe - # MOD_TFE: github.com/hashicorp/terraform-provider-tfe/internal/provider - # MOD_VERSION: github.com/hashicorp/terraform-provider-tfe/version - # run: | - # gotestsum --junitfile summary.xml --format short-verbose -- $MOD_PROVIDER $MOD_TFE $MOD_VERSION -v -timeout=30m -run "${{ steps.test_split.outputs.run }}" + - name: Run Tests + shell: bash + env: + TFE_HOSTNAME: "${{ inputs.hostname }}" + TFE_TOKEN: "${{ inputs.token }}" + TFE_ADMIN_CONFIGURATION_TOKEN: ${{ inputs.admin_configuration_token }} + TFE_ADMIN_PROVISION_LICENSES_TOKEN: ${{ inputs.admin_provision_licenses_token }} + TFE_ADMIN_SECURITY_MAINTENANCE_TOKEN: ${{ inputs.admin_security_maintenance_token }} + TFE_ADMIN_SITE_ADMIN_TOKEN: ${{ inputs.admin_site_admin_token }} + TFE_ADMIN_SUBSCRIPTION_TOKEN: ${{ inputs.admin_subscription_token }} + TFE_ADMIN_SUPPORT_TOKEN: ${{ inputs.admin_support_token }} + TFE_ADMIN_VERSION_MAINTENANCE_TOKEN: ${{ inputs.admin_version_maintenance_token }} + TFE_USER1: tfe-provider-user1 + TFE_USER2: tfe-provider-user2 + TF_ACC: "1" + ENABLE_TFE: "${{ inputs.enterprise }}" + RUN_TASKS_URL: "http://testing-mocks.tfe:22180/runtasks/pass" + GITHUB_POLICY_SET_IDENTIFIER: "hashicorp/test-policy-set" + GITHUB_REGISTRY_MODULE_IDENTIFIER: "hashicorp/terraform-random-module" + GITHUB_WORKSPACE_IDENTIFIER: "hashicorp/terraform-random-module" + GITHUB_WORKSPACE_BRANCH: "main" + GITHUB_TOKEN: "${{ inputs.testing-github-token }}" + MOD_PROVIDER: github.com/hashicorp/terraform-provider-tfe + MOD_TFE: github.com/hashicorp/terraform-provider-tfe/internal/provider + MOD_VERSION: github.com/hashicorp/terraform-provider-tfe/version + run: | + gotestsum --junitfile summary.xml --format short-verbose -- $MOD_PROVIDER $MOD_TFE $MOD_VERSION -v -timeout=30m -run "${{ steps.test_split.outputs.run }}" - # - name: Upload test artifacts - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: junit-test-summary-${{ matrix.index }} - # path: summary.xml - # retention-days: 1 + - name: Upload test artifacts + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: junit-test-summary-${{ matrix.index }} + path: summary.xml + retention-days: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38c84a302..353bfe5c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,13 +9,6 @@ concurrency: cancel-in-progress: true jobs: - notice: - name: TESTS ARE TEMPORARILY DISABLED- RUN CHANGED RESOURCE TESTS LOCALLY - runs-on: ubuntu-latest - steps: - - name: NO-OP - run: | - echo "Tests are skipped. Please test manually." lint: name: lint runs-on: ubuntu-latest @@ -31,8 +24,8 @@ jobs: fail-fast: false matrix: # If you adjust these parameters, also adjust the jrm input files on the "Merge reports" step below - total: [ 1 ] - index: [ 0 ] + total: [ 5 ] + index: [ 0, 1, 2, 3, 4 ] steps: - name: Fetch Outputs id: tflocal @@ -80,7 +73,7 @@ jobs: run: npm install -g junit-report-merger - name: Merge reports - run: jrm ./ci-summary-provider.xml "junit-test-summary-0/*.xml" + run: jrm ./ci-summary-provider.xml "junit-test-summary-0/*.xml" "junit-test-summary-1/*.xml" "junit-test-summary-2/*.xml" "junit-test-summary-3/*.xml" "junit-test-summary-4/*.xml" - name: Upload test artifacts uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 510d30ce1..6b4a58c11 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Set up Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/nightly-tfe-test.yml b/.github/workflows/nightly-tfe-test.yml index 1c46a5d0f..980d3eb55 100644 --- a/.github/workflows/nightly-tfe-test.yml +++ b/.github/workflows/nightly-tfe-test.yml @@ -25,8 +25,8 @@ jobs: strategy: fail-fast: false matrix: - total: [ 1 ] - index: [ 0 ] + total: [ 5 ] + index: [ 0, 1, 2, 3, 4 ] steps: - name: Fetch Outputs id: tflocal @@ -99,7 +99,7 @@ jobs: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Set up Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: go-version-file: go.mod check-latest: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eac34d5c..437e91e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ENHANCEMENTS: * `r/tfe_oauth_client`: Add Bitbucket Data Center support with the `bitbucket_data_center` option for `service_provider` by @zainq11 [#1303](https://github.com/hashicorp/terraform-provider-tfe/pull/1304) +* `r/tfe_workspace`: Add an `auto_destroy_at` attribute for scheduling an auto-destroy run in the future, by @notchairmk [1354](https://github.com/hashicorp/terraform-provider-tfe/pull/1354) +* `d/tfe_workspace`: Add an `auto_destroy_at` attribute for reading a scheduled auto-destroy, by @notchairmk [1354](https://github.com/hashicorp/terraform-provider-tfe/pull/1354) * `r/tfe_registry_module`: Add `initial_version` support for Branch Based Modules by @aaabdelgany [#1363](https://github.com/hashicorp/terraform-provider-tfe/pull/1363) ## v0.55.0 diff --git a/internal/provider/data_source_workspace.go b/internal/provider/data_source_workspace.go index 7b7cb3210..40f3c330e 100644 --- a/internal/provider/data_source_workspace.go +++ b/internal/provider/data_source_workspace.go @@ -52,6 +52,11 @@ func dataSourceTFEWorkspace() *schema.Resource { Computed: true, }, + "auto_destroy_at": { + Type: schema.TypeString, + Computed: true, + }, + "file_triggers_enabled": { Type: schema.TypeBool, Computed: true, @@ -240,6 +245,12 @@ func dataSourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error d.Set("operations", workspace.Operations) d.Set("policy_check_failures", workspace.PolicyCheckFailures) + autoDestroyAt, err := flattenAutoDestroyAt(workspace.AutoDestroyAt) + if err != nil { + return fmt.Errorf("Error flattening auto destroy during read: %w", err) + } + d.Set("auto_destroy_at", autoDestroyAt) + // If target tfe instance predates projects, then workspace.Project will be nil if workspace.Project != nil { d.Set("project_id", workspace.Project.ID) diff --git a/internal/provider/data_source_workspace_test.go b/internal/provider/data_source_workspace_test.go index 2f3a2c158..57d096653 100644 --- a/internal/provider/data_source_workspace_test.go +++ b/internal/provider/data_source_workspace_test.go @@ -171,6 +171,25 @@ func TestAccTFEWorkspaceDataSourceWithTriggerPatterns(t *testing.T) { }) } +func TestAccTFEWorkspaceDataSource_readAutoDestroyAt(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspaceDataSourceConfig_basic(rInt), + Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_at", ""), + }, + { + Config: testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroy(rInt), + Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"), + }, + }, + }) +} + func TestAccTFEWorkspaceDataSource_readProjectIDDefault(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -243,6 +262,44 @@ data "tfe_workspace" "foobar" { }`, rInt, rInt, aart) } +func testAccTFEWorkspaceDataSourceConfig_basic(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test-%d" + organization = tfe_organization.foobar.id + description = "provider-testing" +} + +data "tfe_workspace" "foobar" { + name = tfe_workspace.foobar.name + organization = tfe_workspace.foobar.organization +}`, rInt, rInt) +} + +func testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroy(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test-%d" + organization = tfe_organization.foobar.id + description = "provider-testing" + auto_destroy_at = "2100-01-01T00:00:00Z" +} + +data "tfe_workspace" "foobar" { + name = tfe_workspace.foobar.name + organization = tfe_workspace.foobar.organization +}`, rInt, rInt) +} func testAccTFEWorkspaceDataSourceConfigWithTriggerPatterns(workspaceName string, organizationName string) string { return fmt.Sprintf(` data "tfe_workspace" "foobar" { diff --git a/internal/provider/helper_test.go b/internal/provider/helper_test.go index 56a46e4f0..407d6d56b 100644 --- a/internal/provider/helper_test.go +++ b/internal/provider/helper_test.go @@ -200,6 +200,18 @@ func skipUnlessBeta(t *testing.T) { } } +// Temporarily skip a test that may be experiencing API errors. This method +// purposefully errors after the set date to remind contributors to remove this check +// and verify that the API errors are no longer occurring. +func skipUnlessAfterDate(t *testing.T, d time.Time) { + today := time.Now() + if today.After(d) { + t.Fatalf("This test was temporarily skipped and has now expired. Remove this check to run this test.") + } else { + t.Skipf("Temporarily skipping test due to external issues: %s", t.Name()) + } +} + func enterpriseEnabled() bool { return os.Getenv("ENABLE_TFE") == "1" } @@ -255,9 +267,9 @@ func randomString(t *testing.T) string { return v } -type retryableFn func() (interface{}, error) +type retryableFn func() (any, error) -func retryFn(maxRetries, secondsBetween int, f retryableFn) (interface{}, error) { +func retryFn(maxRetries, secondsBetween int, f retryableFn) (any, error) { tick := time.NewTicker(time.Duration(secondsBetween) * time.Second) retries := 0 diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index d758d37e2..1e1cc0f29 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -213,6 +213,8 @@ func TestConfigureEnvOrganization(t *testing.T) { // The TFE Provider tests use these environment variables, which are set in the // GitHub Action workflow file .github/workflows/ci.yml. func testAccGithubPreCheck(t *testing.T) { + skipUnlessAfterDate(t, time.Date(2024, 5, 24, 0, 0, 0, 0, time.UTC)) + if envGithubToken == "" { t.Skip("Please set GITHUB_TOKEN to run this test") } diff --git a/internal/provider/resource_tfe_policy_set_test.go b/internal/provider/resource_tfe_policy_set_test.go index 9f26b0bd6..dc5a5da29 100644 --- a/internal/provider/resource_tfe_policy_set_test.go +++ b/internal/provider/resource_tfe_policy_set_test.go @@ -4,6 +4,7 @@ package provider import ( + "context" "fmt" "os" "regexp" @@ -60,6 +61,19 @@ func TestAccTFEPolicySet_pinnedPolicyRuntimeVersion(t *testing.T) { sha := genSentinelSha(t, "secret", "data") version := genSafeRandomSentinelVersion() + adminClient := testAdminClient(t, versionMaintenanceAdmin) + + opts := tfe.AdminSentinelVersionCreateOptions{ + Version: version, + SHA: sha, + URL: "https://hashicorp.com", + } + + tool, err := adminClient.Admin.SentinelVersions.Create(context.Background(), opts) + if err != nil { + t.Fatal(err) + } + org, orgCleanup := createBusinessOrganization(t, tfeClient) t.Cleanup(orgCleanup) @@ -71,7 +85,7 @@ func TestAccTFEPolicySet_pinnedPolicyRuntimeVersion(t *testing.T) { CheckDestroy: testAccCheckTFEPolicySetDestroy, Steps: []resource.TestStep{ { - Config: testAccTFEPolicySet_pinnedPolicyRuntimeVersion(org.Name, version, sha), + Config: testAccTFEPolicySet_pinnedPolicyRuntimeVersion(org.Name, tool.Version), Check: resource.ComposeTestCheckFunc( testAccCheckTFEPolicySetExists("tfe_policy_set.foobar", policySet), testAccCheckTFEPolicySetAttributes(policySet), @@ -1034,14 +1048,8 @@ resource "tfe_policy_set" "foobar" { }`, organization, organization) } -func testAccTFEPolicySet_pinnedPolicyRuntimeVersion(organization string, version string, sha string) string { +func testAccTFEPolicySet_pinnedPolicyRuntimeVersion(organization string, version string) string { return fmt.Sprintf(` -resource "tfe_sentinel_version" "foobar" { - version = "%s" - url = "https://www.hashicorp.com" - sha = "%s" -} - resource "tfe_sentinel_policy" "foo" { name = "policy-foo" policy = "main = rule { true }" @@ -1055,7 +1063,7 @@ resource "tfe_policy_set" "foobar" { agent_enabled = true policy_tool_version = "%s" policy_ids = [tfe_sentinel_policy.foo.id] -}`, version, sha, organization, organization, version) +}`, organization, organization, version) } func testAccTFEPolicySetOPA_basic(organization string, version string, sha string) string { diff --git a/internal/provider/resource_tfe_registry_module_test.go b/internal/provider/resource_tfe_registry_module_test.go index 51ab42119..c6edd527d 100644 --- a/internal/provider/resource_tfe_registry_module_test.go +++ b/internal/provider/resource_tfe_registry_module_test.go @@ -934,6 +934,8 @@ func testAccCheckTFERegistryModuleDestroy(s *terraform.State) error { } func testAccPreCheckTFERegistryModule(t *testing.T) { + skipUnlessAfterDate(t, time.Date(2024, 5, 24, 0, 0, 0, 0, time.UTC)) + if envGithubToken == "" { t.Skip("Please set GITHUB_TOKEN to run this test") } diff --git a/internal/provider/resource_tfe_test_variable_test.go b/internal/provider/resource_tfe_test_variable_test.go index e66478cee..ecf5e4395 100644 --- a/internal/provider/resource_tfe_test_variable_test.go +++ b/internal/provider/resource_tfe_test_variable_test.go @@ -19,7 +19,10 @@ func TestAccTFETestVariable_basic(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccGithubPreCheck(t) + }, ProtoV5ProviderFactories: testAccMuxedProviders, CheckDestroy: testAccCheckTFETestVariableDestroy, Steps: []resource.TestStep{ @@ -52,7 +55,10 @@ func TestAccTFETestVariable_update(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { + testAccPreCheck(t) + testAccGithubPreCheck(t) + }, ProtoV5ProviderFactories: testAccMuxedProviders, CheckDestroy: testAccCheckTFETestVariableDestroy, Steps: []resource.TestStep{ @@ -199,7 +205,7 @@ resource "tfe_organization" "foobar" { name = "tst-terraform-%d" email = "admin@company.com" } - + resource "tfe_oauth_client" "foobar" { organization = tfe_organization.foobar.name api_url = "https://api.github.com" diff --git a/internal/provider/resource_tfe_workspace.go b/internal/provider/resource_tfe_workspace.go index f72155e9a..c9d1100d6 100644 --- a/internal/provider/resource_tfe_workspace.go +++ b/internal/provider/resource_tfe_workspace.go @@ -19,6 +19,7 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/jsonapi" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -109,6 +110,11 @@ func resourceTFEWorkspace() *schema.Resource { Default: false, }, + "auto_destroy_at": { + Type: schema.TypeString, + Optional: true, + }, + "execution_mode": { Type: schema.TypeString, Optional: true, @@ -340,6 +346,14 @@ func resourceTFEWorkspaceCreate(d *schema.ResourceData, meta interface{}) error } } + if _, ok := d.GetOk("auto_destroy_at"); ok { + autoDestroyAt, err := expandAutoDestroyAt(d) + if err != nil { + return fmt.Errorf("Error expanding auto destroy during create: %w", err) + } + options.AutoDestroyAt = autoDestroyAt + } + if v, ok := d.GetOk("execution_mode"); ok { executionMode := tfe.String(v.(string)) options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{ @@ -533,6 +547,12 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error { } d.Set("agent_pool_id", agentPoolID) + autoDestroyAt, err := flattenAutoDestroyAt(workspace.AutoDestroyAt) + if err != nil { + return fmt.Errorf("Error flattening auto destroy during read: %w", err) + } + d.Set("auto_destroy_at", autoDestroyAt) + var tagNames []interface{} managedTags := d.Get("tag_names").(*schema.Set) for _, tagName := range workspace.TagNames { @@ -585,7 +605,7 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error d.HasChange("operations") || d.HasChange("execution_mode") || d.HasChange("description") || d.HasChange("agent_pool_id") || d.HasChange("global_remote_state") || d.HasChange("structured_run_output_enabled") || - d.HasChange("assessments_enabled") || d.HasChange("project_id") { + d.HasChange("assessments_enabled") || d.HasChange("project_id") || d.HasChange("auto_destroy_at") { // Create a new options struct. options := tfe.WorkspaceUpdateOptions{ Name: tfe.String(d.Get("name").(string)), @@ -638,6 +658,14 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("auto_destroy_at") { + autoDestroyAt, err := expandAutoDestroyAt(d) + if err != nil { + return fmt.Errorf("Error expanding auto destroy during update: %w", err) + } + options.AutoDestroyAt = autoDestroyAt + } + if d.HasChange("execution_mode") { if v, ok := d.GetOk("execution_mode"); ok { options.ExecutionMode = tfe.String(v.(string)) @@ -933,6 +961,35 @@ func validateAgentExecution(_ context.Context, d *schema.ResourceDiff) error { return nil } +func expandAutoDestroyAt(d *schema.ResourceData) (jsonapi.NullableAttr[time.Time], error) { + v, ok := d.GetOk("auto_destroy_at") + + if !ok { + return jsonapi.NewNullNullableAttr[time.Time](), nil + } + + autoDestroyAt, err := time.Parse(time.RFC3339, v.(string)) + if err != nil { + return nil, err + } + + return jsonapi.NewNullableAttrWithValue(autoDestroyAt), nil +} + +func flattenAutoDestroyAt(a jsonapi.NullableAttr[time.Time]) (*string, error) { + if !a.IsSpecified() { + return nil, nil + } + + autoDestroyTime, err := a.Get() + if err != nil { + return nil, err + } + + autoDestroyAt := autoDestroyTime.Format(time.RFC3339) + return &autoDestroyAt, nil +} + func validTagName(tag string) bool { // Tags are re-validated here because the API will accept uppercase letters and automatically // downcase them, causing resource drift. It's better to catch this issue during the plan phase diff --git a/internal/provider/resource_tfe_workspace_run_test.go b/internal/provider/resource_tfe_workspace_run_test.go index c076987cd..23bd7e7ef 100644 --- a/internal/provider/resource_tfe_workspace_run_test.go +++ b/internal/provider/resource_tfe_workspace_run_test.go @@ -16,6 +16,10 @@ import ( ) func TestAccTFEWorkspaceRun_withApplyOnlyBlock(t *testing.T) { + // Currently, tflocal cloud box is incapable of running terraform more than once at a time + // due to the use of the raw_exec nomad driver. + skipUnlessAfterDate(t, time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC)) + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() tfeClient, err := getClientUsingEnv() @@ -65,6 +69,10 @@ func TestAccTFEWorkspaceRun_withApplyOnlyBlock(t *testing.T) { } func TestAccTFEWorkspaceRun_withBothApplyAndDestroyBlocks(t *testing.T) { + // Currently, tflocal cloud box is incapable of running terraform more than once at a time + // due to the use of the raw_exec nomad driver. + skipUnlessAfterDate(t, time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC)) + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() tfeClient, err := getClientUsingEnv() @@ -258,19 +266,27 @@ func testAccCheckTFEWorkspaceRunDestroy(workspaceID string, expectedDestroyCount return func(s *terraform.State) error { config := testAccProvider.Meta().(ConfiguredClient) - runList, err := config.Client.Runs.List(ctx, workspaceID, &tfe.RunListOptions{ - Operation: "destroy", - Status: string(tfe.RunApplied), + mustBeNil, err := retryFn(10, 1, func() (any, error) { + runList, err := config.Client.Runs.List(ctx, workspaceID, &tfe.RunListOptions{ + Operation: "destroy", + }) + if err != nil { + return nil, fmt.Errorf("Unable to find destroy run, %w", err) + } + + if len(runList.Items) != expectedDestroyCount { + return nil, fmt.Errorf("Expected %d destroy runs but found %d", expectedDestroyCount, len(runList.Items)) + } + + return nil, nil }) - if err != nil { - return fmt.Errorf("Unable to find destroy run, %w", err) - } - if len(runList.Items) != expectedDestroyCount { - return fmt.Errorf("Expected %d destroy runs but found %d", expectedDestroyCount, len(runList.Items)) + // This just makes the unparam linter happy and will always be nil + if mustBeNil != nil { + return fmt.Errorf("expected mustBeNil to be nil, but was %v", mustBeNil) } - return nil + return err } } diff --git a/internal/provider/resource_tfe_workspace_test.go b/internal/provider/resource_tfe_workspace_test.go index 59c6afaec..3f24cf199 100644 --- a/internal/provider/resource_tfe_workspace_test.go +++ b/internal/provider/resource_tfe_workspace_test.go @@ -2648,6 +2648,52 @@ func TestAccTFEWorkspace_basicAssessmentsEnabled(t *testing.T) { }) } +func TestAccTFEWorkspace_createWithAutoDestroyAt(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspace_basicWithAutoDestroyAt(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists("tfe_workspace.foobar", &tfe.Workspace{}, testAccProvider), + resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"), + ), + }, + }, + }) +} + +func TestAccTFEWorkspace_updateWithAutoDestroyAt(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEWorkspace_basic(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEWorkspaceExists("tfe_workspace.foobar", &tfe.Workspace{}, testAccProvider), + resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", ""), + ), + }, + { + Config: testAccTFEWorkspace_basicWithAutoDestroyAt(rInt), + Check: resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"), + }, + { + Config: testAccTFEWorkspace_basic(rInt), + Check: resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", ""), + }, + }, + }) +} + func TestAccTFEWorkspace_createWithSourceURL(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -2924,6 +2970,22 @@ resource "tfe_workspace" "foobar" { }`, rInt) } +func testAccTFEWorkspace_basicWithAutoDestroyAt(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test" + organization = tfe_organization.foobar.id + auto_apply = true + file_triggers_enabled = false + auto_destroy_at = "2100-01-01T00:00:00Z" +}`, rInt) +} + func testAccTFEWorkspace_operationsTrue(organization string) string { return fmt.Sprintf(` resource "tfe_workspace" "foobar" { diff --git a/website/docs/d/workspace.html.markdown b/website/docs/d/workspace.html.markdown index da6018846..f44e8aa67 100644 --- a/website/docs/d/workspace.html.markdown +++ b/website/docs/d/workspace.html.markdown @@ -35,6 +35,7 @@ In addition to all arguments above, the following attributes are exported: * `allow_destroy_plan` - Indicates whether destroy plans can be queued on the workspace. * `auto_apply` - Indicates whether to automatically apply changes when a Terraform plan is successful. * `auto_apply_run_trigger` - Whether the workspace will automatically apply changes for runs that were created by run triggers from another workspace. +* `auto_destroy_at` - Future date/time string at which point all resources in a workspace will be scheduled to be deleted. * `assessments_enabled` - (Available only in HCP Terraform) Indicates whether health assessments such as drift detection are enabled for the workspace. * `file_triggers_enabled` - Indicates whether runs are triggered based on the changed files in a VCS push (if `true`) or always triggered on every push (if `false`). * `global_remote_state` - (Optional) Whether the workspace should allow all workspaces in the organization to access its state data during runs. If false, then only specifically approved workspaces can access its state (determined by the `remote_state_consumer_ids` argument). diff --git a/website/docs/r/workspace.html.markdown b/website/docs/r/workspace.html.markdown index 879e9f43a..9688c0d06 100644 --- a/website/docs/r/workspace.html.markdown +++ b/website/docs/r/workspace.html.markdown @@ -68,6 +68,10 @@ The following arguments are supported: * `assessments_enabled` - (Optional) Whether to regularly run health assessments such as drift detection on the workspace. Defaults to `false`. * `auto_apply` - (Optional) Whether to automatically apply changes when a Terraform plan is successful. Defaults to `false`. * `auto_apply_run_trigger` - (Optional) Whether to automatically apply changes for runs that were created by run triggers from another workspace. Defaults to `false`. +* `auto_destroy_at` - (Optional) A future date/time string at which point all resources in a workspace will be scheduled for deletion. Must be a string in RFC3339 format (e.g. "2100-01-01T00:00:00Z"). + +~> **NOTE:** `auto_destroy_at` is not intended for workspaces containing production resources or long-lived workspaces. Since this attribute is in-part managed by HCP Terraform, using `ignore_changes` for this attribute may be preferred. + * `description` - (Optional) A description for the workspace. * `execution_mode` - (Optional) **Deprecated** Which [execution mode](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#execution-mode) to use. Use [tfe_workspace_settings](workspace_settings) instead. * `file_triggers_enabled` - (Optional) Whether to filter runs based on the changed files