diff --git a/github/acc_test.go b/github/acc_test.go index 3bb6309244..dd990030b3 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -74,15 +74,21 @@ type testAccConfig struct { var testAccConf *testAccConfig +var testAccProviders map[string]*schema.Provider = map[string]*schema.Provider{ + "github": Provider(), +} + // providerFactories are used to instantiate a provider during acceptance testing. // The factory function will be invoked for every Terraform CLI command executed // to create a provider server to which the CLI can reattach. -var providerFactories = map[string]func() (*schema.Provider, error){ - //nolint:unparam - "github": func() (*schema.Provider, error) { - return Provider(), nil - }, -} +var ( + providerFactories = map[string]func() (*schema.Provider, error){ + //nolint:unparam + "github": func() (*schema.Provider, error) { + return Provider(), nil + }, + } +) func TestMain(m *testing.M) { authMode := testMode(os.Getenv("GH_TEST_AUTH_MODE")) diff --git a/github/provider_test.go b/github/provider_test.go index 6d05f3fa15..d2f064f205 100644 --- a/github/provider_test.go +++ b/github/provider_test.go @@ -6,33 +6,9 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -var ( - testAccProviders map[string]*schema.Provider - testAccProviderFactories func(providers *[]*schema.Provider) map[string]func() (*schema.Provider, error) - testAccProvider *schema.Provider -) - -func init() { - testAccProvider = Provider() - testAccProviders = map[string]*schema.Provider{ - "github": testAccProvider, - } - testAccProviderFactories = func(providers *[]*schema.Provider) map[string]func() (*schema.Provider, error) { - return map[string]func() (*schema.Provider, error){ - //nolint:unparam - "github": func() (*schema.Provider, error) { - p := Provider() - *providers = append(*providers, p) - return p, nil - }, - } - } -} - func TestProvider(t *testing.T) { t.Run("runs internal validation without error", func(t *testing.T) { if err := Provider().InternalValidate(); err != nil { diff --git a/github/resource_github_actions_environment_secret.go b/github/resource_github_actions_environment_secret.go index d0b9f4c3a1..ab8a4f2518 100644 --- a/github/resource_github_actions_environment_secret.go +++ b/github/resource_github_actions_environment_secret.go @@ -4,26 +4,42 @@ import ( "context" "encoding/base64" "errors" + "fmt" "log" "net/http" "net/url" + "strconv" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceGithubActionsEnvironmentSecret() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsEnvironmentSecretCreateOrUpdate, - Read: resourceGithubActionsEnvironmentSecretRead, - Delete: resourceGithubActionsEnvironmentSecretDelete, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsEnvironmentSecretV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsEnvironmentSecretStateUpgradeV0, + Version: 0, + }, + }, + + CustomizeDiff: resourceGithubActionsEnvironmentSecretDiff, + CreateContext: resourceGithubActionsEnvironmentSecretCreate, + ReadContext: resourceGithubActionsEnvironmentSecretRead, + UpdateContext: resourceGithubActionsEnvironmentSecretUpdate, + DeleteContext: resourceGithubActionsEnvironmentSecretDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsEnvironmentSecretImport, + }, Schema: map[string]*schema.Schema{ "repository": { Type: schema.TypeString, Required: true, - ForceNew: true, Description: "Name of the repository.", }, "environment": { @@ -39,22 +55,26 @@ func resourceGithubActionsEnvironmentSecret() *schema.Resource { Description: "Name of the secret.", ValidateDiagFunc: validateSecretNameFunc, }, + "key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "ID of the public key used to encrypt the secret.", + ConflictsWith: []string{"plaintext_value"}, + }, "encrypted_value": { Type: schema.TypeString, Optional: true, - ForceNew: true, - Sensitive: true, Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", - ConflictsWith: []string{"plaintext_value"}, + ExactlyOneOf: []string{"encrypted_value", "plaintext_value"}, ValidateDiagFunc: toDiagFunc(validation.StringIsBase64, "encrypted_value"), }, "plaintext_value": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Sensitive: true, - Description: "Plaintext value of the secret to be encrypted.", - ConflictsWith: []string{"encrypted_value"}, + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Plaintext value of the secret to be encrypted.", + ExactlyOneOf: []string{"encrypted_value", "plaintext_value"}, }, "created_at": { Type: schema.TypeString, @@ -66,161 +86,352 @@ func resourceGithubActionsEnvironmentSecret() *schema.Resource { Computed: true, Description: "Date of 'actions_environment_secret' update.", }, + "remote_updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of remote 'actions_environment_secret' update.", + }, }, } } -func resourceGithubActionsEnvironmentSecretCreateOrUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() +func resourceGithubActionsEnvironmentSecretDiff(ctx context.Context, diff *schema.ResourceDiff, m any) error { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + if len(diff.Id()) == 0 { + return nil + } + + if diff.HasChange("repository") { + repoIDString, _, _, _, err := parseID4(diff.Id()) + if err != nil { + return err + } + + repoID, err := strconv.Atoi(repoIDString) + if err != nil { + return fmt.Errorf("failed to convert repository ID %s to integer: %w", repoIDString, err) + } + + repoName := diff.Get("repository").(string) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode != http.StatusNotFound { + return err + } + + log.Printf("[WARN] Repository %s not found when checking repository change for actions environment variable %s so assuming a rename", repoName, diff.Id()) + } else { + return err + } + } else { + if repoID != int(repo.GetID()) { + return diff.ForceNew("repository") + } + } + } + + if diff.HasChange("remote_updated_at") { + remoteUpdatedAt := diff.Get("remote_updated_at").(string) + if len(remoteUpdatedAt) == 0 { + return nil + } + + updatedAt := diff.Get("updated_at").(string) + if updatedAt != remoteUpdatedAt { + if len(updatedAt) == 0 { + return diff.SetNew("updated_at", remoteUpdatedAt) + } + + return diff.SetNewComputed("updated_at") + } + } + + return nil +} + +func resourceGithubActionsEnvironmentSecretCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name repoName := d.Get("repository").(string) envName := d.Get("environment").(string) - escapedEnvName := url.PathEscape(envName) secretName := d.Get("secret_name").(string) - plaintextValue := d.Get("plaintext_value").(string) - var encryptedValue string + keyID := d.Get("key_id").(string) + encryptedValue := d.Get("encrypted_value").(string) + + escapedEnvName := url.QueryEscape(envName) repo, _, err := client.Repositories.Get(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } + repoID := int(repo.GetID()) - keyId, publicKey, err := getEnvironmentPublicKeyDetails(repo.GetID(), escapedEnvName, meta) - if err != nil { - return err + var publicKey string + if len(keyID) == 0 || len(encryptedValue) == 0 { + keyID, publicKey, err = getEnvironmentPublicKeyDetails(ctx, meta, repoID, escapedEnvName) + if err != nil { + return diag.FromErr(err) + } } - if encryptedText, ok := d.GetOk("encrypted_value"); ok { - encryptedValue = encryptedText.(string) - } else { + if len(encryptedValue) == 0 { + plaintextValue := d.Get("plaintext_value").(string) + encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) if err != nil { - return err + return diag.FromErr(err) } encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) } - // Create an EncryptedSecret and encrypt the plaintext value into it - eSecret := &github.EncryptedSecret{ + _, err = client.Actions.CreateOrUpdateEnvSecret(ctx, repoID, escapedEnvName, &github.EncryptedSecret{ Name: secretName, - KeyID: keyId, + KeyID: keyID, EncryptedValue: encryptedValue, + }) + if err != nil { + return diag.FromErr(err) } - _, err = client.Actions.CreateOrUpdateEnvSecret(ctx, int(repo.GetID()), escapedEnvName, eSecret) - if err != nil { - return err + d.SetId(buildID(strconv.Itoa(repoID), repoName, escapedEnvName, secretName)) + + if err := d.Set("key_id", keyID); err != nil { + return diag.FromErr(err) + } + + // GitHub API does not return on create so we have to lookup the secret to get timestamps + if secret, _, err := client.Actions.GetEnvSecret(ctx, repoID, escapedEnvName, secretName); err == nil { + if err := d.Set("created_at", secret.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", secret.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } } - d.SetId(buildThreePartID(repoName, envName, secretName)) - return resourceGithubActionsEnvironmentSecretRead(d, meta) + return nil } -func resourceGithubActionsEnvironmentSecretRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() +func resourceGithubActionsEnvironmentSecretRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client - repoName, envName, secretName, err := parseThreePartID(d.Id(), "repository", "environment", "secret_name") + repoIDString, _, _, _, err := parseID4(d.Id()) if err != nil { - return err + return diag.FromErr(err) } - escapedEnvName := url.PathEscape(envName) - repo, _, err := client.Repositories.Get(ctx, owner, repoName) + repoID, err := strconv.Atoi(repoIDString) if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub", - d.Id()) - d.SetId("") - return nil - } - } - return err + return diag.FromErr(fmt.Errorf("failed to convert repository ID %s to integer: %w", repoIDString, err)) } - secret, _, err := client.Actions.GetEnvSecret(ctx, int(repo.GetID()), escapedEnvName, secretName) + repoName := d.Get("repository").(string) + envName := d.Get("environment").(string) + secretName := d.Get("secret_name").(string) + + escapedEnvName := url.QueryEscape(envName) + + secret, _, err := client.Actions.GetEnvSecret(ctx, repoID, escapedEnvName, secretName) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub", - d.Id()) + log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub", d.Id()) d.SetId("") return nil } } - return err - } - - if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil { - return err - } - if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil { - return err - } - if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { - return err - } - - // This is a drift detection mechanism based on timestamps. - // - // If we do not currently store the "updated_at" field, it means we've only - // just created the resource and the value is most likely what we want it to - // be. - // - // 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 this case, we can no longer - // trust that the value matches what is in the state file. - // - // 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.Set("encrypted_value", "") - _ = d.Set("plaintext_value", "") - } else if !ok { + return diag.FromErr(err) + } + + d.SetId(buildID(repoIDString, repoName, escapedEnvName, secretName)) + if len(d.Get("created_at").(string)) == 0 { + if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + } + + if len(d.Get("updated_at").(string)) == 0 { if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { - return err + return diag.FromErr(err) } } + if err = d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } + return nil } -func resourceGithubActionsEnvironmentSecretDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) +func resourceGithubActionsEnvironmentSecretUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client - repoName, envName, secretName, err := parseThreePartID(d.Id(), "repository", "environment", "secret_name") + repoIDString, _, _, _, err := parseID4(d.Id()) if err != nil { - return err + return diag.FromErr(err) } - escapedEnvName := url.PathEscape(envName) - repo, _, err := client.Repositories.Get(ctx, owner, repoName) + + repoID, err := strconv.Atoi(repoIDString) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to convert repository ID %s to integer: %w", repoIDString, err)) + } + + repoName := d.Get("repository").(string) + envName := d.Get("environment").(string) + secretName := d.Get("secret_name").(string) + keyID := d.Get("key_id").(string) + encryptedValue := d.Get("encrypted_value").(string) + + escapedEnvName := url.QueryEscape(envName) + + var publicKey string + if len(keyID) == 0 || len(encryptedValue) == 0 { + keyID, publicKey, err = getEnvironmentPublicKeyDetails(ctx, meta, repoID, escapedEnvName) + if err != nil { + return diag.FromErr(err) + } + } + + if len(encryptedValue) == 0 { + plaintextValue := d.Get("plaintext_value").(string) + + encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) + if err != nil { + return diag.FromErr(err) + } + encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) + } + + _, err = client.Actions.CreateOrUpdateEnvSecret(ctx, repoID, escapedEnvName, &github.EncryptedSecret{ + Name: secretName, + KeyID: keyID, + EncryptedValue: encryptedValue, + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildID(repoIDString, repoName, escapedEnvName, secretName)) + if err := d.Set("key_id", keyID); err != nil { + return diag.FromErr(err) + } + + // GitHub API does not return on update so we have to lookup the secret to get timestamps + if secret, _, err := client.Actions.GetEnvSecret(ctx, repoID, escapedEnvName, secretName); err == nil { + if err := d.Set("created_at", secret.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", secret.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("updated_at", nil); err != nil { + return diag.FromErr(err) + } + if err := d.Set("remote_updated_at", nil); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubActionsEnvironmentSecretDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + + repoIDString, _, escapedEnvName, secretName, err := parseID4(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + repoID, err := strconv.Atoi(repoIDString) if err != nil { - return err + return diag.FromErr(fmt.Errorf("failed to convert repository ID %s to integer: %w", repoIDString, err)) } + log.Printf("[INFO] Deleting environment secret: %s", d.Id()) - _, err = client.Actions.DeleteEnvSecret(ctx, int(repo.GetID()), escapedEnvName, secretName) + _, err = client.Actions.DeleteEnvSecret(ctx, repoID, escapedEnvName, secretName) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubActionsEnvironmentSecretImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName, escapedEnvName, secretName, err := parseID3(d.Id()) + if err != nil { + return nil, err + } + + envName, err := url.QueryUnescape(escapedEnvName) + if err != nil { + return nil, err + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, err + } + repoID := int(repo.GetID()) + + secret, _, err := client.Actions.GetEnvSecret(ctx, repoID, escapedEnvName, secretName) + if err != nil { + return nil, err + } + + d.SetId(buildID(strconv.Itoa(repoID), repoName, escapedEnvName, secretName)) + if err := d.Set("repository", repoName); err != nil { + return nil, err + } + if err := d.Set("environment", envName); err != nil { + return nil, err + } + if err := d.Set("secret_name", secretName); err != nil { + return nil, err + } + if err := d.Set("created_at", secret.CreatedAt.String()); err != nil { + return nil, err + } + if err := d.Set("updated_at", secret.UpdatedAt.String()); err != nil { + return nil, err + } + if err := d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil { + return nil, err + } - return err + return []*schema.ResourceData{d}, nil } -func getEnvironmentPublicKeyDetails(repoID int64, envName string, meta any) (keyId, pkValue string, err error) { - client := meta.(*Owner).v3client - ctx := context.Background() +func getEnvironmentPublicKeyDetails(ctx context.Context, meta *Owner, repoID int, envName string) (string, string, error) { + client := meta.v3client - publicKey, _, err := client.Actions.GetEnvPublicKey(ctx, int(repoID), envName) + publicKey, _, err := client.Actions.GetEnvPublicKey(ctx, repoID, envName) if err != nil { - return keyId, pkValue, err + return "", "", err } - return publicKey.GetKeyID(), publicKey.GetKey(), err + return publicKey.GetKeyID(), publicKey.GetKey(), nil } diff --git a/github/resource_github_actions_environment_secret_migration.go b/github/resource_github_actions_environment_secret_migration.go new file mode 100644 index 0000000000..1dbb21a225 --- /dev/null +++ b/github/resource_github_actions_environment_secret_migration.go @@ -0,0 +1,97 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/url" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubActionsEnvironmentSecretV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository.", + }, + "environment": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the environment.", + }, + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the secret.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "encrypted_value": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", + ConflictsWith: []string{"plaintext_value"}, + ValidateDiagFunc: toDiagFunc(validation.StringIsBase64, "encrypted_value"), + }, + "plaintext_value": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Plaintext value of the secret to be encrypted.", + ConflictsWith: []string{"encrypted_value"}, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_environment_secret' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_environment_secret' update.", + }, + }, + } +} + +func resourceGithubActionsEnvironmentSecretStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + log.Printf("[DEBUG] GitHub Actions Environment Secret Attributes before migration: %#v", rawState) + + repoName, ok := rawState["repository"].(string) + if !ok { + return nil, fmt.Errorf("repository not found or is not a string") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err) + } + + envName, ok := rawState["environment"].(string) + if !ok { + return nil, fmt.Errorf("environment not found or is not a string") + } + + secretName, ok := rawState["secret_name"].(string) + if !ok { + return nil, fmt.Errorf("secret_name not found or is not a string") + } + + rawState["id"] = buildID(strconv.Itoa(int(repo.GetID())), repoName, url.QueryEscape(envName), secretName) + + log.Printf("[DEBUG] GitHub Actions Environment Secret Attributes after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_environment_secret_migration_test.go b/github/resource_github_actions_environment_secret_migration_test.go new file mode 100644 index 0000000000..03286cd7e8 --- /dev/null +++ b/github/resource_github_actions_environment_secret_migration_test.go @@ -0,0 +1,52 @@ +package github + +// TODO: Enable this test one there is a way to mock the metadata. + +// import ( +// "context" +// "reflect" +// "testing" +// ) + +// func Test_resourceGithubActionsEnvironmentSecretStateUpgradeV0(t *testing.T) { +// t.Parallel() + +// for _, d := range []struct { +// testName string +// rawState map[string]any +// want map[string]any +// shouldError bool +// }{ +// { +// testName: "migrates v0 to v1", +// rawState: map[string]any{ +// "id": "my-repo:my-environment:MY_SECRET", +// "repository": "my-repo", +// "environment": "my-environment", +// "secret_name": "MY_SECRET", +// "value": "my-value", +// }, +// want: map[string]any{ +// "id": "123456:my-repo:my-environment:MY_SECRET", +// "repository": "my-repo", +// "environment": "my-environment", +// "secret_name": "MY_SECRET", +// "value": "my-value", +// }, +// shouldError: false, +// }, +// } { +// t.Run(d.testName, func(t *testing.T) { +// t.Parallel() + +// got, err := resourceGithubActionsEnvironmentSecretStateUpgradeV0(context.Background(), d.rawState, nil) +// if (err != nil) != d.shouldError { +// t.Fatalf("unexpected error state") +// } + +// if !d.shouldError && !reflect.DeepEqual(got, d.want) { +// t.Fatalf("got %+v, want %+v", got, d.want) +// } +// }) +// } +// } diff --git a/github/resource_github_actions_environment_secret_test.go b/github/resource_github_actions_environment_secret_test.go index 3164547ae4..0fc3e17f13 100644 --- a/github/resource_github_actions_environment_secret_test.go +++ b/github/resource_github_actions_environment_secret_test.go @@ -1,82 +1,43 @@ package github import ( + "context" "encoding/base64" "fmt" - "strings" + "net/url" "testing" + "github.com/google/go-github/v81/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccGithubActionsEnvironmentSecret(t *testing.T) { - t.Run("creates and updates secrets without error", func(t *testing.T) { + t.Run("create_plaintext", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-secret-%s", testResourcePrefix, randomID) - secretValue := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) - updatedSecretValue := base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + value := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - } - - 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" - } - - 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" - } - `, repoName, secretValue, secretValue) - - 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", - updatedSecretValue, - ), - resource.TestCheckResourceAttr( - "github_actions_environment_secret.encrypted_secret", "encrypted_value", - updatedSecretValue, - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_secret.plaintext_secret", "created_at", - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_secret.plaintext_secret", "updated_at", - ), - ), - } +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "%s" +} +`, repoName, envName, secretName, value) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -84,175 +45,662 @@ func TestAccGithubActionsEnvironmentSecret(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: checks["before"], + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "plaintext_value", value), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "encrypted_value"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("create_with_env_name_id_separator_character", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "env:test" + secretName := "test" + value := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "%s" +} +`, repoName, envName, secretName, value) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "plaintext_value", value), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "encrypted_value"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("create_update_plaintext", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + value := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + updatedValue := base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")) + + config := ` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "%s" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, envName, secretName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "plaintext_value", value), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "encrypted_value"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), }, { - Config: strings.Replace(config, - secretValue, - updatedSecretValue, 2), - Check: checks["after"], + Config: fmt.Sprintf(config, repoName, envName, secretName, updatedValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "plaintext_value", updatedValue), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "encrypted_value"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), }, }, }) }) - t.Run("deletes secrets without error", func(t *testing.T) { + t.Run("create_update_encrypted", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-secret-%s", testResourcePrefix, randomID) - secretValue := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + value := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + updatedValue := base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")) + + config := ` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + encrypted_value = "%s" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, envName, secretName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "plaintext_value"), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "encrypted_value", value), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, repoName, envName, secretName, updatedValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "plaintext_value"), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "encrypted_value", updatedValue), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("create_update_encrypted_with_key", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + value := base64.StdEncoding.EncodeToString([]byte("super_secret_value")) + updatedValue := base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")) + + config := ` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +data "github_actions_environment_public_key" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + key_id = data.github_actions_environment_public_key.test.key_id + secret_name = "%s" + encrypted_value = "%s" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, envName, secretName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "plaintext_value"), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "encrypted_value", value), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, repoName, envName, secretName, updatedValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "secret_name", secretName), + resource.TestCheckNoResourceAttr("github_actions_environment_secret.test", "plaintext_value"), + resource.TestCheckResourceAttr("github_actions_environment_secret.test", "encrypted_value", updatedValue), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "key_id"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("update_on_drift", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - } - - 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" - } - - 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" - } - `, repoName, secretValue, secretValue) +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "test" +} +`, repoName, envName, secretName) + + var beforeCreatedAt string resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, - Destroy: true, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "remote_updated_at"), + func(s *terraform.State) error { + beforeCreatedAt = s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + return nil + }, + ), + }, + { + PreConfig: func() { + meta, err := getTestMeta() + if err != nil { + t.Fatal(err.Error()) + } + client := meta.v3client + owner := meta.name + ctx := context.Background() + + escapedEnvName := url.PathEscape(envName) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + t.Fatal(err.Error()) + } + repoID := int(repo.GetID()) + + keyID, _, err := getEnvironmentPublicKeyDetails(ctx, meta, repoID, escapedEnvName) + if err != nil { + t.Fatal(err.Error()) + } + + _, err = client.Actions.CreateOrUpdateEnvSecret(ctx, repoID, escapedEnvName, &github.EncryptedSecret{ + Name: secretName, + EncryptedValue: base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")), + KeyID: keyID, + }) + if err != nil { + t.Fatal(err.Error()) + } + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "remote_updated_at"), + func(s *terraform.State) error { + afterCreatedAt := s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + + if afterCreatedAt != beforeCreatedAt { + return fmt.Errorf("expected resource to be updated, but created_at has been modified: %s", beforeCreatedAt) + } + return nil + }, + ), + }, + }, + }) + }) + + t.Run("lifecycle_can_ignore_drift", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "test" + + lifecycle { + ignore_changes = [remote_updated_at] + } +} +`, repoName, envName, secretName) + + var beforeUpdatedAt string + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "remote_updated_at"), + func(s *terraform.State) error { + beforeUpdatedAt = s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["updated_at"] + return nil + }, + ), + }, + { + PreConfig: func() { + meta, err := getTestMeta() + if err != nil { + t.Fatal(err.Error()) + } + client := meta.v3client + owner := meta.name + ctx := context.Background() + + escapedEnvName := url.PathEscape(envName) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + t.Fatal(err.Error()) + } + repoID := int(repo.GetID()) + + keyID, _, err := getEnvironmentPublicKeyDetails(ctx, meta, repoID, escapedEnvName) + if err != nil { + t.Fatal(err.Error()) + } + + _, err = client.Actions.CreateOrUpdateEnvSecret(ctx, repoID, escapedEnvName, &github.EncryptedSecret{ + Name: secretName, + EncryptedValue: base64.StdEncoding.EncodeToString([]byte("updated_super_secret_value")), + KeyID: keyID, + }) + if err != nil { + t.Fatal(err.Error()) + } + }, + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "remote_updated_at"), + func(s *terraform.State) error { + afterUpdatedAt := s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["updated_at"] + + if afterUpdatedAt != beforeUpdatedAt { + return fmt.Errorf("expected resource to ignore drift, but updated_at has been modified: %s", beforeUpdatedAt) + } + return nil + }, + ), }, }, }) }) + + t.Run("update_renamed_repo", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + updatedRepoName := fmt.Sprintf("%s%s-updated", testResourcePrefix, randomID) + + // TODO: Remove lifecycle ignore_changes block when repo rename is supported + config := ` +resource "github_repository" "test" { + name = "%s" } -func TestAccGithubActionsEnvironmentSecretIgnoreChanges(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-secret-ic-%s", testResourcePrefix, randomID) - - 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 = "%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", - ), - ), - } +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" + + lifecycle { + ignore_changes = all + } +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "test" + plaintext_value = "test" +} +` + var beforeCreatedAt string resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnauthenticated(t) }, - Providers: testAccProviders, + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(configFmtStr, repoName, secretValue, secretValue), - Check: checks["before"], + Config: fmt.Sprintf(config, repoName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + func(s *terraform.State) error { + beforeCreatedAt = s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + return nil + }, + ), }, { - Config: fmt.Sprintf(configFmtStr, repoName, secretValue, secretValue), - Check: checks["after"], + Config: fmt.Sprintf(config, updatedRepoName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + func(s *terraform.State) error { + afterCreatedAt := s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + + if afterCreatedAt != beforeCreatedAt { + return fmt.Errorf("expected resource to not be recreated, but created_at has been modified: %s", beforeCreatedAt) + } + return nil + }, + ), }, + }, + }) + }) + + t.Run("recreate_changed_repo", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + repoName2 := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" +} + +resource "github_repository" "test2" { + name = "%s" +} + +resource "github_repository_environment" "test2" { + repository = github_repository.test2.name + environment = "test" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "test" + plaintext_value = "test" +} +`, repoName, repoName2) + + configUpdated := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" +} + +resource "github_repository" "test2" { + name = "%s" +} + +resource "github_repository_environment" "test2" { + repository = github_repository.test2.name + environment = "test" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test2.name + environment = github_repository_environment.test2.environment + secret_name = "test" + plaintext_value = "test" +} +`, repoName, repoName2) + + var beforeCreatedAt string + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ { - // 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, repoName, modifiedSecretValue, modifiedSecretValue), + Config: config, 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. - ), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + func(s *terraform.State) error { + beforeCreatedAt = s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + return nil + }, ), }, + { + Config: configUpdated, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_secret.test", "updated_at"), + func(s *terraform.State) error { + afterCreatedAt := s.RootModule().Resources["github_actions_environment_secret.test"].Primary.Attributes["created_at"] + + if afterCreatedAt == beforeCreatedAt { + return fmt.Errorf("expected resource to be recreated, but created_at has not been modified: %s", beforeCreatedAt) + } + return nil + }, + ), + }, + }, + }) + }) + + t.Run("destroy", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + } + + resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" + } + + resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "test" + plaintext_value = "test" + } +`, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Destroy: true, + }, + }, + }) + }) + + t.Run("import", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + secretName := "test" + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_secret" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + secret_name = "%s" + plaintext_value = "test" +} +`, repoName, envName, secretName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: "github_actions_environment_secret.test", + ImportStateId: buildID(repoName, envName, secretName), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"key_id", "plaintext_value"}, + }, }, }) }) diff --git a/github/resource_github_actions_environment_variable.go b/github/resource_github_actions_environment_variable.go index ac6990ce53..043ba00ed3 100644 --- a/github/resource_github_actions_environment_variable.go +++ b/github/resource_github_actions_environment_variable.go @@ -3,22 +3,35 @@ package github import ( "context" "errors" + "fmt" "log" "net/http" "net/url" + "strconv" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubActionsEnvironmentVariable() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsEnvironmentVariableCreateOrUpdate, - Read: resourceGithubActionsEnvironmentVariableRead, - Update: resourceGithubActionsEnvironmentVariableCreateOrUpdate, - Delete: resourceGithubActionsEnvironmentVariableDelete, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsEnvironmentVariableV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsEnvironmentVariableStateUpgradeV0, + Version: 0, + }, + }, + + CustomizeDiff: resourceGithubActionsEnvironmentVariableDiff, + CreateContext: resourceGithubActionsEnvironmentVariableCreate, + ReadContext: resourceGithubActionsEnvironmentVariableRead, + UpdateContext: resourceGithubActionsEnvironmentVariableUpdate, + DeleteContext: resourceGithubActionsEnvironmentVariableDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubActionsEnvironmentVariableImport, }, Schema: map[string]*schema.Schema{ @@ -59,88 +72,238 @@ func resourceGithubActionsEnvironmentVariable() *schema.Resource { } } -func resourceGithubActionsEnvironmentVariableCreateOrUpdate(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() - - repoName := d.Get("repository").(string) - envName := d.Get("environment").(string) - escapedEnvName := url.PathEscape(envName) - name := d.Get("variable_name").(string) +func resourceGithubActionsEnvironmentVariableDiff(ctx context.Context, diff *schema.ResourceDiff, m any) error { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - variable := &github.ActionsVariable{ - Name: name, - Value: d.Get("value").(string), + if len(diff.Id()) == 0 { + return nil } - // Try to create the variable first - _, err := client.Actions.CreateEnvVariable(ctx, owner, repoName, escapedEnvName, variable) - if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusConflict { - // Variable already exists, try to update instead - // If it fails here, we want to return the error otherwise continue - _, err = client.Actions.UpdateEnvVariable(ctx, owner, repoName, escapedEnvName, variable) - if err != nil { + if diff.HasChange("repository") { + repoIDString, _, _, _, err := parseID4(diff.Id()) + if err != nil { + return err + } + + repoID, err := strconv.Atoi(repoIDString) + if err != nil { + return fmt.Errorf("failed to convert repository ID %s to integer: %w", repoIDString, err) + } + + repoName := diff.Get("repository").(string) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode != http.StatusNotFound { + return err + } + + log.Printf("[WARN] Repository %s not found when checking repository change for actions environment variable %s so assuming a rename", repoName, diff.Id()) + } else { return err } } else { - return err + if repoID != int(repo.GetID()) { + return diff.ForceNew("repository") + } } } - d.SetId(buildThreePartID(repoName, envName, name)) - return resourceGithubActionsEnvironmentVariableRead(d, meta) + return nil } -func resourceGithubActionsEnvironmentVariableRead(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.Background() +func resourceGithubActionsEnvironmentVariableCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Get("repository").(string) + envName := d.Get("environment").(string) + varName := d.Get("variable_name").(string) + + escapedEnvName := url.QueryEscape(envName) + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return diag.FromErr(err) + } + repoID := int(repo.GetID()) - repoName, envName, name, err := parseThreePartID(d.Id(), "repository", "environment", "variable_name") + _, err = client.Actions.CreateEnvVariable(ctx, owner, repoName, escapedEnvName, &github.ActionsVariable{ + Name: varName, + Value: d.Get("value").(string), + }) if err != nil { - return err + return diag.FromErr(err) + } + + d.SetId(buildID(strconv.Itoa(repoID), repoName, escapedEnvName, varName)) + + // GitHub API does not return on create so we have to lookup the variable to get timestamps + if variable, _, err := client.Actions.GetEnvVariable(ctx, owner, repoName, escapedEnvName, varName); err == nil { + if err := d.Set("created_at", variable.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", variable.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } } - escapedEnvName := url.PathEscape(envName) - variable, _, err := client.Actions.GetEnvVariable(ctx, owner, repoName, escapedEnvName, name) + return nil +} + +func resourceGithubActionsEnvironmentVariableRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName := d.Get("repository").(string) + envName := d.Get("environment").(string) + varName := d.Get("variable_name").(string) + + escapedEnvName := url.QueryEscape(envName) + + variable, _, err := client.Actions.GetEnvVariable(ctx, owner, repoName, escapedEnvName, varName) if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[INFO] Removing actions variable %s from state because it no longer exists in GitHub", - d.Id()) + log.Printf("[INFO] Removing actions variable %s from state because it no longer exists in GitHub", d.Id()) d.SetId("") return nil } } - return err + return diag.FromErr(err) } - _ = d.Set("repository", repoName) - _ = d.Set("environment", envName) - _ = d.Set("variable_name", name) - _ = d.Set("value", variable.Value) - _ = d.Set("created_at", variable.CreatedAt.String()) - _ = d.Set("updated_at", variable.UpdatedAt.String()) + repoIDString, _, _, _, err := parseID4(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildID(repoIDString, repoName, escapedEnvName, varName)) + if err = d.Set("value", variable.Value); err != nil { + return diag.FromErr(err) + } + if err = d.Set("created_at", variable.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("updated_at", variable.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } return nil } -func resourceGithubActionsEnvironmentVariableDelete(d *schema.ResourceData, meta any) error { - client := meta.(*Owner).v3client - owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) +func resourceGithubActionsEnvironmentVariableUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name - repoName, envName, name, err := parseThreePartID(d.Id(), "repository", "environment", "variable_name") + repoName := d.Get("repository").(string) + envName := d.Get("environment").(string) + name := d.Get("variable_name").(string) + + escapedEnvName := url.QueryEscape(envName) + + _, err := client.Actions.UpdateEnvVariable(ctx, owner, repoName, escapedEnvName, &github.ActionsVariable{ + Name: name, + Value: d.Get("value").(string), + }) + if err != nil { + return diag.FromErr(err) + } + + repoIDString, _, _, _, err := parseID4(d.Id()) + if err != nil { + return diag.FromErr(err) + } + d.SetId(buildID(repoIDString, repoName, escapedEnvName, name)) + + // GitHub API does not return on create so we have to lookup the variable to get timestamps + if variable, _, err := client.Actions.GetEnvVariable(ctx, owner, repoName, escapedEnvName, name); err == nil { + if err := d.Set("created_at", variable.CreatedAt.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("updated_at", variable.UpdatedAt.String()); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("updated_at", nil); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubActionsEnvironmentVariableDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + _, repoName, escapedEnvName, varName, err := parseID4(d.Id()) if err != nil { - return err + return diag.FromErr(err) } - escapedEnvName := url.PathEscape(envName) - _, err = client.Actions.DeleteEnvVariable(ctx, owner, repoName, escapedEnvName, name) + _, err = client.Actions.DeleteEnvVariable(ctx, owner, repoName, escapedEnvName, varName) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubActionsEnvironmentVariableImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + repoName, escapedEnvName, varName, err := parseID3(d.Id()) + if err != nil { + return nil, err + } + + envName, err := url.QueryUnescape(escapedEnvName) + if err != nil { + return nil, err + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, err + } + repoID := int(repo.GetID()) + + variable, _, err := client.Actions.GetEnvVariable(ctx, owner, repoName, escapedEnvName, varName) + if err != nil { + return nil, err + } + + d.SetId(buildID(strconv.Itoa(repoID), repoName, escapedEnvName, varName)) + if err := d.Set("repository", repoName); err != nil { + return nil, err + } + if err := d.Set("environment", envName); err != nil { + return nil, err + } + if err := d.Set("variable_name", varName); err != nil { + return nil, err + } + if err := d.Set("value", variable.Value); err != nil { + return nil, err + } + if err := d.Set("created_at", variable.CreatedAt.String()); err != nil { + return nil, err + } + if err := d.Set("updated_at", variable.UpdatedAt.String()); err != nil { + return nil, err + } - return err + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_actions_environment_variable_migration.go b/github/resource_github_actions_environment_variable_migration.go new file mode 100644 index 0000000000..3ff5383ff4 --- /dev/null +++ b/github/resource_github_actions_environment_variable_migration.go @@ -0,0 +1,87 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/url" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubActionsEnvironmentVariableV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository.", + }, + "environment": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the environment.", + }, + "variable_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the variable.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "value": { + Type: schema.TypeString, + Required: true, + Description: "Value of the variable.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_variable' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_variable' update.", + }, + }, + } +} + +func resourceGithubActionsEnvironmentVariableStateUpgradeV0(ctx context.Context, rawState map[string]any, m any) (map[string]any, error) { + meta := m.(*Owner) + client := meta.v3client + owner := meta.name + + log.Printf("[DEBUG] GitHub Actions Environment Variable Attributes before migration: %#v", rawState) + + repoName, ok := rawState["repository"].(string) + if !ok { + return nil, fmt.Errorf("repository not found or is not a string") + } + + repo, _, err := client.Repositories.Get(ctx, owner, repoName) + if err != nil { + return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err) + } + + envName, ok := rawState["environment"].(string) + if !ok { + return nil, fmt.Errorf("environment not found or is not a string") + } + + varName, ok := rawState["variable_name"].(string) + if !ok { + return nil, fmt.Errorf("variable_name not found or is not a string") + } + + rawState["id"] = buildID(strconv.Itoa(int(repo.GetID())), repoName, url.QueryEscape(envName), varName) + + log.Printf("[DEBUG] GitHub Actions Environment Variable Attributes after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_environment_variable_migration_test.go b/github/resource_github_actions_environment_variable_migration_test.go new file mode 100644 index 0000000000..eb2be83e49 --- /dev/null +++ b/github/resource_github_actions_environment_variable_migration_test.go @@ -0,0 +1,52 @@ +package github + +// TODO: Enable this test one there is a way to mock the metadata. + +// import ( +// "context" +// "reflect" +// "testing" +// ) + +// func Test_resourceGithubActionsEnvironmentVariableStateUpgradeV0(t *testing.T) { +// t.Parallel() + +// for _, d := range []struct { +// testName string +// rawState map[string]any +// want map[string]any +// shouldError bool +// }{ +// { +// testName: "migrates v0 to v1", +// rawState: map[string]any{ +// "id": "my-repo:my-environment:MY_VARIABLE", +// "repository": "my-repo", +// "environment": "my-environment", +// "variable_name": "MY_VARIABLE", +// "value": "my-value", +// }, +// want: map[string]any{ +// "id": "123456:my-repo:my-environment:MY_VARIABLE", +// "repository": "my-repo", +// "environment": "my-environment", +// "variable_name": "MY_VARIABLE", +// "value": "my-value", +// }, +// shouldError: false, +// }, +// } { +// t.Run(d.testName, func(t *testing.T) { +// t.Parallel() + +// got, err := resourceGithubActionsEnvironmentVariableStateUpgradeV0(context.Background(), d.rawState, nil) +// if (err != nil) != d.shouldError { +// t.Fatalf("unexpected error state") +// } + +// if !d.shouldError && !reflect.DeepEqual(got, d.want) { +// t.Fatalf("got %+v, want %+v", got, d.want) +// } +// }) +// } +// } diff --git a/github/resource_github_actions_environment_variable_test.go b/github/resource_github_actions_environment_variable_test.go index 2a2325eacc..11485ed275 100644 --- a/github/resource_github_actions_environment_variable_test.go +++ b/github/resource_github_actions_environment_variable_test.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "net/url" - "strings" + "regexp" "testing" "github.com/google/go-github/v81/github" @@ -14,100 +14,331 @@ import ( ) func TestAccGithubActionsEnvironmentVariable(t *testing.T) { - t.Run("creates and updates environment variables without error", func(t *testing.T) { + t.Run("create", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-var-%s", testResourcePrefix, randomID) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + varName := "test" + value := "my_variable_value" + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "%s" + value = "%s" +} +`, repoName, envName, varName, value) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "value", value), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("create_with_env_name_id_separator_character", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "env:test" + varName := "test" + value := "my_variable_value" + + config := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "%s" + value = "%s" +} +`, repoName, envName, varName, value) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "value", value), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("create_update", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + varName := "test" value := "my_variable_value" updatedValue := "my_updated_variable_value" + config := ` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "%s" + value = "%s" +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName, envName, varName, value), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "value", value), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + ), + }, + { + Config: fmt.Sprintf(config, repoName, envName, varName, updatedValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "environment", envName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "variable_name", varName), + resource.TestCheckResourceAttr("github_actions_environment_variable.test", "value", updatedValue), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + ), + }, + }, + }) + }) + + t.Run("update_renamed_repo", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + updatedRepoName := fmt.Sprintf("%s%s-updated", testResourcePrefix, randomID) + + // TODO: Remove lifecycle ignore_changes block when repo rename is supported + config := ` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" + + lifecycle { + ignore_changes = all + } +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "test" + value = "test" +} +` + + var beforeCreatedAt string + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, repoName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + func(s *terraform.State) error { + beforeCreatedAt = s.RootModule().Resources["github_actions_environment_variable.test"].Primary.Attributes["created_at"] + return nil + }, + ), + }, + { + Config: fmt.Sprintf(config, updatedRepoName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + func(s *terraform.State) error { + afterCreatedAt := s.RootModule().Resources["github_actions_environment_variable.test"].Primary.Attributes["created_at"] + + if afterCreatedAt != beforeCreatedAt { + return fmt.Errorf("expected resource to not be recreated, but created_at has been modified: %s", beforeCreatedAt) + } + return nil + }, + ), + }, + }, + }) + }) + + t.Run("recreate_changed_repo", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + repoName2 := fmt.Sprintf("%supdated-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - } - - resource "github_repository_environment" "test" { - repository = github_repository.test.name - environment = "environment / test" - } - - resource "github_actions_environment_variable" "variable" { - repository = github_repository.test.name - environment = github_repository_environment.test.environment - variable_name = "test_variable" - value = "%s" - } - `, repoName, value) - - checks := map[string]resource.TestCheckFunc{ - "before": resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_environment_variable.variable", "value", - value, - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_variable.variable", "created_at", - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_variable.variable", "updated_at", - ), - ), - "after": resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_environment_variable.variable", "value", - updatedValue, - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_variable.variable", "created_at", - ), - resource.TestCheckResourceAttrSet( - "github_actions_environment_variable.variable", "updated_at", - ), - ), - } +resource "github_repository" "test" { + name = "%s" +} +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" +} + +resource "github_repository" "test2" { + name = "%s" +} + +resource "github_repository_environment" "test2" { + repository = github_repository.test2.name + environment = "test" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "test_variable" + value = "test" +} +`, repoName, repoName2) + + configUpdated := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" +} + +resource "github_repository" "test2" { + name = "%s" +} + +resource "github_repository_environment" "test2" { + repository = github_repository.test2.name + environment = "test" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test2.name + environment = github_repository_environment.test2.environment + variable_name = "test_variable" + value = "test" +} +`, repoName, repoName2) + + var beforeCreatedAt string resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: checks["before"], + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + func(s *terraform.State) error { + beforeCreatedAt = s.RootModule().Resources["github_actions_environment_variable.test"].Primary.Attributes["created_at"] + return nil + }, + ), }, { - Config: strings.Replace(config, - value, - updatedValue, 1), - Check: checks["after"], + Config: configUpdated, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "created_at"), + resource.TestCheckResourceAttrSet("github_actions_environment_variable.test", "updated_at"), + func(s *terraform.State) error { + afterCreatedAt := s.RootModule().Resources["github_actions_environment_variable.test"].Primary.Attributes["created_at"] + + if afterCreatedAt == beforeCreatedAt { + return fmt.Errorf("expected resource to be recreated, but created_at has not been modified: %s", beforeCreatedAt) + } + return nil + }, + ), }, }, }) }) - t.Run("deletes environment variables without error", func(t *testing.T) { + t.Run("destroy", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-var-%s", testResourcePrefix, randomID) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - } - - resource "github_repository_environment" "test" { - repository = github_repository.test.name - environment = "environment / test" - } - - resource "github_actions_environment_variable" "variable" { - repository = github_repository.test.name - environment = github_repository_environment.test.environment - variable_name = "test_variable" - value = "my_variable_value" - } - `, repoName) +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "test" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "test_variable" + value = "my_variable_value" +} +`, repoName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ + { + Config: config, + }, { Config: config, Destroy: true, @@ -116,30 +347,30 @@ func TestAccGithubActionsEnvironmentVariable(t *testing.T) { }) }) - t.Run("imports environment variables without error", func(t *testing.T) { + t.Run("import", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-var-%s", testResourcePrefix, randomID) - value := "my_variable_value" - envName := "environment / test" + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" varName := "test_variable" + value := "my_variable_value" config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - } - - resource "github_repository_environment" "test" { - repository = github_repository.test.name - environment = "%s" - } - - resource "github_actions_environment_variable" "variable" { - repository = github_repository.test.name - environment = github_repository_environment.test.environment - variable_name = "%s" - value = "%s" - } - `, repoName, envName, varName, value) +resource "github_repository" "test" { + name = "%s" +} + +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "%s" + value = "%s" +} +`, repoName, envName, varName, value) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnauthenticated(t) }, @@ -149,83 +380,86 @@ func TestAccGithubActionsEnvironmentVariable(t *testing.T) { Config: config, }, { - ResourceName: "github_actions_environment_variable.variable", - ImportStateId: fmt.Sprintf(`%s:%s:%s`, repoName, envName, varName), + ResourceName: "github_actions_environment_variable.test", + ImportStateId: buildID(repoName, envName, varName), ImportState: true, ImportStateVerify: true, }, }, }) }) + + t.Run("error_on_existing", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + envName := "test" + varName := "test_variable" + + baseConfig := fmt.Sprintf(` +resource "github_repository" "test" { + name = "%s" } -func TestAccGithubActionsEnvironmentVariable_alreadyExists(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%srepo-act-env-var-exist-%s", testResourcePrefix, randomID) - envName := "environment / test" - varName := "test_variable" - value := "my_variable_value" - - config := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - vulnerability_alerts = true - } - - resource "github_repository_environment" "test" { - repository = github_repository.test.name - environment = "%s" - } - - resource "github_actions_environment_variable" "variable" { - repository = github_repository.test.name - environment = github_repository_environment.test.environment - variable_name = "%s" - value = "%s" - } - `, repoName, envName, varName, value) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnauthenticated(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - // First, create the repository and environment. - Config: fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - vulnerability_alerts = true - } +resource "github_repository_environment" "test" { + repository = github_repository.test.name + environment = "%s" +} +`, repoName, envName) + + config := fmt.Sprintf(` +%s + +resource "github_actions_environment_variable" "test" { + repository = github_repository.test.name + environment = github_repository_environment.test.environment + variable_name = "%s" + value = "test" +} +`, baseConfig, varName) - resource "github_repository_environment" "test" { - repository = github_repository.test.name - environment = "%s" + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: baseConfig, + Check: func(*terraform.State) error { + meta, err := getTestMeta() + if err != nil { + return err } - `, repoName, envName), - Check: resource.ComposeTestCheckFunc( - func(s *terraform.State) error { - // Now that the repo and env are created, create the variable using the API. - client := testAccProvider.Meta().(*Owner).v3client - owner := testAccProvider.Meta().(*Owner).name + client := meta.v3client + owner := meta.name ctx := context.Background() + escapedEnvName := url.PathEscape(envName) - variable := &github.ActionsVariable{ + _, err = client.Actions.CreateEnvVariable(ctx, owner, repoName, escapedEnvName, &github.ActionsVariable{ Name: varName, - Value: value, + Value: "test", + }) + return err + }, + }, + { + Config: config, + ExpectError: regexp.MustCompile(`Variable already exists`), + Check: func(*terraform.State) error { + meta, err := getTestMeta() + if err != nil { + return err } - _, err := client.Actions.CreateEnvVariable(ctx, owner, repoName, escapedEnvName, variable) + client := meta.v3client + owner := meta.name + ctx := context.Background() + + escapedEnvName := url.PathEscape(envName) + + _, err = client.Actions.DeleteEnvVariable(ctx, owner, repoName, escapedEnvName, varName) return err }, - ), - }, - { - // Now, run the full config. Terraform should detect the existing variable and "adopt" it. - Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_actions_environment_variable.variable", "value", value), - ), + }, }, - }, + }) }) } diff --git a/github/resource_github_actions_organization_secret_test.go b/github/resource_github_actions_organization_secret_test.go index 9af4548cb1..905e8b1844 100644 --- a/github/resource_github_actions_organization_secret_test.go +++ b/github/resource_github_actions_organization_secret_test.go @@ -182,12 +182,16 @@ func TestAccGithubActionsOrganizationSecret_DestroyOnDrift(t *testing.T) { if !ok { t.Errorf("not found: github_actions_organization_secret.test_secret") } + meta, err := getTestMeta() + if err != nil { + return err + } // Now that the secret is created, update it to trigger a drift. - client := testAccProvider.Meta().(*Owner).v3client - owner := testAccProvider.Meta().(*Owner).name + client := meta.v3client + owner := meta.name ctx := t.Context() - keyId, publicKey, err := getOrganizationPublicKeyDetails(owner, testAccProvider.Meta().(*Owner)) + keyId, publicKey, err := getOrganizationPublicKeyDetails(owner, meta) if err != nil { t.Errorf("Failed to get organization public key details: %v", err) } diff --git a/github/util.go b/github/util.go index 693bf55d0f..ad93a1257d 100644 --- a/github/util.go +++ b/github/util.go @@ -19,9 +19,56 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +const idSeparator = ":" + // https://developer.github.com/guides/traversing-with-pagination/#basics-of-pagination var maxPerPage = 100 +// buildID joins the parts with the idSeparator. +func buildID(parts ...string) string { + return strings.Join(parts, idSeparator) +} + +// parseID splits the id by the idSeparator checking the count. +func parseID(id string, count int) ([]string, error) { + parts := strings.Split(id, idSeparator) + if len(parts) != count { + return nil, fmt.Errorf("unexpected ID format (%q); expected %d parts separated by %q", id, count, idSeparator) + } + + return parts, nil +} + +// // parseID2 splits the id by the idSeparator into two parts. +// func parseID2(id string) (string, string, error) { +// parts, err := parseID(id, 2) +// if err != nil { +// return "", "", err +// } + +// return parts[0], parts[1], nil +// } + +// parseID3 splits the id by the idSeparator into three parts. +func parseID3(id string) (string, string, string, error) { + parts, err := parseID(id, 3) + if err != nil { + return "", "", "", err + } + + return parts[0], parts[1], parts[2], nil +} + +// parseID4 splits the id by the idSeparator into four parts. +func parseID4(id string) (string, string, string, string, error) { + parts, err := parseID(id, 4) + if err != nil { + return "", "", "", "", err + } + + return parts[0], parts[1], parts[2], parts[3], nil +} + func checkOrganization(meta any) error { if !meta.(*Owner).IsOrganization { return fmt.Errorf("this resource can only be used in the context of an organization, %q is a user", meta.(*Owner).name) diff --git a/website/docs/r/actions_environment_secret.html.markdown b/website/docs/r/actions_environment_secret.html.markdown index 8082ab3887..83e9e252ec 100644 --- a/website/docs/r/actions_environment_secret.html.markdown +++ b/website/docs/r/actions_environment_secret.html.markdown @@ -22,61 +22,64 @@ in your code. See below for an example of this abstraction. ## Example Usage ```hcl -resource "github_actions_environment_secret" "example_secret" { - environment = "example_environment" - secret_name = "example_secret_name" - plaintext_value = var.some_secret_string +resource "github_actions_environment_secret" "example_plaintext" { + repository = "example-repo" + environment = "example-environment" + secret_name = "example_secret_name" + plaintext_value = "example-value } -resource "github_actions_environment_secret" "example_secret" { - environment = "example_environment" - secret_name = "example_secret_name" - encrypted_value = var.some_encrypted_secret_string +resource "github_actions_environment_secret" "example_encrypted" { + repository = "example-repo" + environment = "example-environment" + secret_name = "example_secret_name" + key_id = var.key_id + encrypted_value = var.encrypted_secret_string } ``` ```hcl -data "github_repository" "repo" { +data "github_repository" "example" { full_name = "my-org/repo" } -resource "github_repository_environment" "repo_environment" { - repository = data.github_repository.repo.name - environment = "example_environment" +resource "github_repository_environment" "example" { + repository = data.github_repository.example.name + environment = "example-environment" } -resource "github_actions_environment_secret" "test_secret" { - repository = data.github_repository.repo.name - environment = github_repository_environment.repo_environment.environment +resource "github_actions_environment_secret" "example" { + repository = data.github_repository.example.name + environment = github_repository_environment.example.environment secret_name = "test_secret_name" - plaintext_value = "%s" + plaintext_value = "example-value" } ``` ## 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. +This resource supports using the `lifecycle` `ignore_changes` block on `remote_updated_at` to support 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" +resource "github_actions_environment_secret" "example_plaintext_allow_drift" { + repository = "example-repo" + environment = "example-environment" + secret_name = "example_secret_name" + plaintext_value = "placeholder" lifecycle { - ignore_changes = [plaintext_value] + ignore_changes = [remote_updated_at] } } -resource "github_actions_environment_secret" "example_secret" { - environment = "example_environment" - secret_name = "example_secret_name" - encrypted_value = base64sha256("placeholder") +resource "github_actions_environment_secret" "example_encrypted_allow_drift" { + repository = "example-repo" + environment = "example_environment" + secret_name = "example_secret_name" + encrypted_value = base64sha256("placeholder") lifecycle { - ignore_changes = [encrypted_value] + ignore_changes = [remote_updated_at] } } ``` @@ -85,18 +88,38 @@ resource "github_actions_environment_secret" "example_secret" { The following arguments are supported: - -* `repository` - (Required) Name of the repository. -* `environment` - (Required) Name of the environment. -* `secret_name` - (Required) Name of the secret. -* `encrypted_value` - (Optional) Encrypted value of the secret using the GitHub public key in Base64 format. -* `plaintext_value` - (Optional) Plaintext value of the secret to be encrypted. +- `repository` - (Required) Name of the repository. +- `environment` - (Required) Name of the environment. +- `secret_name` - (Required) Name of the secret. +- `key_id` - (Optional) ID of the public key used to encrypt the secret. This should be provided when setting `encrypted_value`, if it isn't then the current public key will be looked up which could cause a missmatch. This conflicts with `plaintext_value`. +- `encrypted_value` - (Optional) Encrypted value of the secret using the GitHub public key in Base64 format. +- `plaintext_value` - (Optional) Plaintext value of the secret to be encrypted. ## Attributes Reference -* `created_at` - Date of actions_environment_secret creation. -* `updated_at` - Date of actions_environment_secret update. +- `created_at` - Date the secret was created. +- `updated_at` - Date the secret was last updated by the provider. +- `remote_updated_at` - Date the secret was last updated in GitHub. ## 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 can be imported using an ID made of the repository name, environment name (URL escaped), and secret name all separated by a `:`. + +### Import Block + +The following import imports a GitHub actions environment secret named `mysecret` for the repo `myrepo` and environment `myenv` to a `github_actions_environment_secret` resource named `example`. + +```hcl +import { + to = github_actions_environment_secret.example + id = "myrepo:myenv:mysecret" +} +``` + +### Import Command + +The following command imports a GitHub actions environment secret named `mysecret` for the repo `myrepo` and environment `myenv` to a `github_actions_environment_secret` resource named `example`. + +```shell +terraform import github_actions_environment_secret.example myrepo:myenv:mysecret +``` diff --git a/website/docs/r/actions_environment_variable.html.markdown b/website/docs/r/actions_environment_variable.html.markdown index 32079c86b1..7591aa7cb9 100644 --- a/website/docs/r/actions_environment_variable.html.markdown +++ b/website/docs/r/actions_environment_variable.html.markdown @@ -13,28 +13,29 @@ You must have write access to a repository to use this resource. ## Example Usage ```hcl -resource "github_actions_environment_variable" "example_variable" { - environment = "example_environment" - variable_name = "example_variable_name" - value = "example_variable_value" +resource "github_actions_environment_variable" "example" { + repository = "example-repo" + environment = "example-environment" + variable_name = "example_variable_name" + value = "example-value" } ``` ```hcl -data "github_repository" "repo" { +data "github_repository" "example" { full_name = "my-org/repo" } -resource "github_repository_environment" "repo_environment" { - repository = data.github_repository.repo.name +resource "github_repository_environment" "example" { + repository = data.github_repository.example.name environment = "example_environment" } -resource "github_actions_environment_variable" "example_variable" { - repository = data.github_repository.repo.name - environment = github_repository_environment.repo_environment.environment - variable_name = "example_variable_name" - value = "example_variable_value" +resource "github_actions_environment_variable" "example" { + repository = data.github_repository.example.name + environment = github_repository_environment.example.environment + variable_name = "example_variable_name" + value = "example-value" } ``` @@ -42,21 +43,35 @@ resource "github_actions_environment_variable" "example_variable" { The following arguments are supported: - -* `repository` - (Required) Name of the repository. -* `environment` - (Required) Name of the environment. -* `variable_name` - (Required) Name of the variable. -* `value` - (Required) Value of the variable +- `repository` - (Required) Name of the repository. +- `environment` - (Required) Name of the environment. +- `variable_name` - (Required) Name of the variable. +- `value` - (Required) Value of the variable. ## Attributes Reference -* `created_at` - Date of actions_environment_secret creation. -* `updated_at` - Date of actions_environment_secret update. +- `created_at` - Date the variable was created. +- `updated_at` - Date the variable was last updated. ## Import -This resource can be imported using an ID made up of the repository name, environment name, and variable name: +This resource can be imported using an ID made of the repository name, environment name (URL escaped), and variable name all separated by a `:`. + +### Import Block +The following import imports a GitHub actions environment variable named `myvariable` for the repo `myrepo` and environment `myenv` to a `github_actions_environment_variable` resource named `example`. + +```hcl +import { + to = github_actions_environment_variable.example + id = "myrepo:myenv:myvariable" +} ``` -$ terraform import github_actions_environment_variable.test_variable myrepo:myenv:myvariable + +### Import Command + +The following command imports a GitHub actions environment variable named `myvariable` for the repo `myrepo` and environment `myenv` to a `github_actions_environment_variable` resource named `example`. + +```shell +terraform import github_actions_environment_variable.example myrepo:myenv:myvariable ```