Skip to content

Fix Support for Github Environment Secrets' Lifecycle Ignore Changes #2651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions github/resource_github_actions_environment_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions github/resource_github_actions_environment_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.<cut>/tf-acc-test-<id>: "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)
})
})
}
30 changes: 29 additions & 1 deletion website/docs/r/actions_environment_secret.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)!
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)!