diff --git a/github/resource_github_actions_environment_secret.go b/github/resource_github_actions_environment_secret.go index 1558084996..58211e7966 100644 --- a/github/resource_github_actions_environment_secret.go +++ b/github/resource_github_actions_environment_secret.go @@ -172,16 +172,15 @@ func resourceGithubActionsEnvironmentSecretRead(d *schema.ResourceData, meta int // // If the resource is changed externally in the meantime then reading back // the last update timestamp will return a result different than the - // timestamp we've persisted in the state. In that case, we can no longer - // trust that the value (which we don't see) is equal to what we've declared - // previously. + // timestamp we've persisted in the state. In this case, we can no longer + // trust that the value matches what is in the state file. // - // The only solution to enforce consistency between is to mark the resource - // as deleted (unset the ID) in order to fix potential drift by recreating - // the resource. + // To solve this, we must unset the values and allow Terraform to decide whether or + // not this resource should be modified or left as-is (ignore_changes). if updatedAt, ok := d.GetOk("updated_at"); ok && updatedAt != secret.UpdatedAt.String() { log.Printf("[INFO] The environment secret %s has been externally updated in GitHub", d.Id()) - d.SetId("") + d.Set("encrypted_value", "") + d.Set("plaintext_value", "") } else if !ok { if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { return err diff --git a/github/resource_github_actions_environment_secret_test.go b/github/resource_github_actions_environment_secret_test.go index 0f55863370..910a10f214 100644 --- a/github/resource_github_actions_environment_secret_test.go +++ b/github/resource_github_actions_environment_secret_test.go @@ -166,5 +166,136 @@ func TestAccGithubActionsEnvironmentSecret(t *testing.T) { }) }) +} + +func TestAccGithubActionsEnvironmentSecretIgnoreChanges(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + t.Run("creates environment secrets using lifecycle ignore_changes", func(t *testing.T) { + secretValue := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + modifiedSecretValue := base64.StdEncoding.EncodeToString([]byte("a_modified_super_secret_value")) + + configFmtStr := ` + resource "github_repository" "test" { + name = "tf-acc-test-%s" + + # TODO: provider appears to have issues destroying repositories while running the tests. + # + # Even with Organization Admin an error is seen: + # Error: DELETE https://api./tf-acc-test-: "403 Must have admin rights to Repository. []" + # + # Workaround to using 'archive_on_destroy' instead. + archive_on_destroy = true + + visibility = "private" + } + + resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "environment / test" + } + + resource "github_actions_environment_secret" "plaintext_secret" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "test_plaintext_secret_name" + plaintext_value = "%s" + + lifecycle { + ignore_changes = [plaintext_value] + } + } + + resource "github_actions_environment_secret" "encrypted_secret" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "test_encrypted_secret_name" + encrypted_value = "%s" + + lifecycle { + ignore_changes = [encrypted_value] + } + } + ` + + checks := map[string]resource.TestCheckFunc{ + "before": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_environment_secret.plaintext_secret", "plaintext_value", + secretValue, + ), + resource.TestCheckResourceAttr( + "github_actions_environment_secret.encrypted_secret", "encrypted_value", + secretValue, + ), + resource.TestCheckResourceAttrSet( + "github_actions_environment_secret.plaintext_secret", "created_at", + ), + resource.TestCheckResourceAttrSet( + "github_actions_environment_secret.plaintext_secret", "updated_at", + ), + ), + "after": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_environment_secret.plaintext_secret", "plaintext_value", + secretValue, + ), + resource.TestCheckResourceAttr( + "github_actions_environment_secret.encrypted_secret", "encrypted_value", + secretValue, + ), + resource.TestCheckResourceAttrSet( + "github_actions_environment_secret.plaintext_secret", "created_at", + ), + resource.TestCheckResourceAttrSet( + "github_actions_environment_secret.plaintext_secret", "updated_at", + ), + ), + } + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(configFmtStr, randomID, secretValue, secretValue), + Check: checks["before"], + }, + { + Config: fmt.Sprintf(configFmtStr, randomID, secretValue, secretValue), + Check: checks["after"], + }, + { + // In this case the values change in the config, but the lifecycle ignore_changes should + // not cause the actual values to be updated. This would also be the case when a secret + // is externally modified (when what is in state does not match what is given). + Config: fmt.Sprintf(configFmtStr, randomID, modifiedSecretValue, modifiedSecretValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_environment_secret.plaintext_secret", "plaintext_value", + secretValue, // Should still have the original value in state. + ), + resource.TestCheckResourceAttr( + "github_actions_environment_secret.encrypted_secret", "encrypted_value", + secretValue, // Should still have the original value in state. + ), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) } diff --git a/website/docs/r/actions_environment_secret.html.markdown b/website/docs/r/actions_environment_secret.html.markdown index 03f5bb88a8..8082ab3887 100644 --- a/website/docs/r/actions_environment_secret.html.markdown +++ b/website/docs/r/actions_environment_secret.html.markdown @@ -53,6 +53,34 @@ resource "github_actions_environment_secret" "test_secret" { } ``` +## Example Lifecycle Ignore Changes + +This resource supports the `lifecycle` `ignore_changes` block. This is for use cases where a secret value is created +using a placeholder value and then modified after creation outside the scope of Terraform. This approach ensures only +the initial placeholder value is referenced in your code and in the resulting state file. + +```hcl +resource "github_actions_environment_secret" "example_secret" { + environment = "example_environment" + secret_name = "example_secret_name" + plaintext_value = "placeholder" + + lifecycle { + ignore_changes = [plaintext_value] + } +} + +resource "github_actions_environment_secret" "example_secret" { + environment = "example_environment" + secret_name = "example_secret_name" + encrypted_value = base64sha256("placeholder") + + lifecycle { + ignore_changes = [encrypted_value] + } +} +``` + ## Argument Reference The following arguments are supported: @@ -71,4 +99,4 @@ The following arguments are supported: ## Import -This resource does not support importing. If you'd like to help contribute it, please visit our [GitHub page](https://github.com/integrations/terraform-provider-github)! \ No newline at end of file +This resource does not support importing. If you'd like to help contribute it, please visit our [GitHub page](https://github.com/integrations/terraform-provider-github)!