diff --git a/github/migrate_github_actions_organization_secret.go b/github/migrate_github_actions_organization_secret.go deleted file mode 100644 index f973ed2842..0000000000 --- a/github/migrate_github_actions_organization_secret.go +++ /dev/null @@ -1,36 +0,0 @@ -package github - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func resourceGithubActionsOrganizationSecretMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Actions Organization Secret State v0; migrating to v1") - return migrateGithubActionsOrganizationSecretStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) - } -} - -func migrateGithubActionsOrganizationSecretStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes before migration: %#v", is.Attributes) - - // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := is.Attributes["destroy_on_drift"]; !ok { - is.Attributes["destroy_on_drift"] = "true" - } - - log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes after State Migration: %#v", is.Attributes) - - return is, nil -} diff --git a/github/migrate_github_actions_organization_secret_test.go b/github/migrate_github_actions_organization_secret_test.go deleted file mode 100644 index ef6f032963..0000000000 --- a/github/migrate_github_actions_organization_secret_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package github - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestMigrateGithubActionsOrganizationSecretStateV0toV1(t *testing.T) { - // Secret without destroy_on_drift should get default value - oldAttributes := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - } - - newState, err := migrateGithubActionsOrganizationSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - "destroy_on_drift": "true", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } - - // Secret with existing destroy_on_drift should be preserved - oldAttributesWithDrift := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "destroy_on_drift": "false", - } - - newState2, err := migrateGithubActionsOrganizationSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributesWithDrift, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributesWithDrift := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "destroy_on_drift": "false", - } - if !reflect.DeepEqual(newState2.Attributes, expectedAttributesWithDrift) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributesWithDrift, newState2.Attributes) - } -} diff --git a/github/migrate_github_actions_secret.go b/github/migrate_github_actions_secret.go deleted file mode 100644 index 9bce957eea..0000000000 --- a/github/migrate_github_actions_secret.go +++ /dev/null @@ -1,36 +0,0 @@ -package github - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func resourceGithubActionsSecretMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Actions Secret State v0; migrating to v1") - return migrateGithubActionsSecretStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) - } -} - -func migrateGithubActionsSecretStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Actions Secret Attributes before migration: %#v", is.Attributes) - - // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := is.Attributes["destroy_on_drift"]; !ok { - is.Attributes["destroy_on_drift"] = "true" - } - - log.Printf("[DEBUG] GitHub Actions Secret Attributes after State Migration: %#v", is.Attributes) - - return is, nil -} diff --git a/github/migrate_github_actions_secret_test.go b/github/migrate_github_actions_secret_test.go deleted file mode 100644 index 9f7374de05..0000000000 --- a/github/migrate_github_actions_secret_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package github - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestMigrateGithubActionsSecretStateV0toV1(t *testing.T) { - // Secret without destroy_on_drift should get default value - oldAttributes := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - } - - newState, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - "destroy_on_drift": "true", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } - - // Secret with existing destroy_on_drift should be preserved - oldAttributesWithDrift := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "destroy_on_drift": "false", - } - - newState2, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributesWithDrift, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributesWithDrift := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "destroy_on_drift": "false", - } - if !reflect.DeepEqual(newState2.Attributes, expectedAttributesWithDrift) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributesWithDrift, newState2.Attributes) - } -} diff --git a/github/migrate_github_repository.go b/github/migrate_github_repository.go deleted file mode 100644 index 098b7ecd43..0000000000 --- a/github/migrate_github_repository.go +++ /dev/null @@ -1,40 +0,0 @@ -package github - -import ( - "fmt" - "log" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func resourceGithubRepositoryMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Repository State v0; migrating to v1") - return migrateGithubRepositoryStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) - } -} - -func migrateGithubRepositoryStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Repository Attributes before migration: %#v", is.Attributes) - - prefix := "branches." - - for k := range is.Attributes { - if strings.HasPrefix(k, prefix) { - delete(is.Attributes, k) - } - } - - log.Printf("[DEBUG] GitHub Repository Attributes after State Migration: %#v", is.Attributes) - - return is, nil -} diff --git a/github/migrate_github_repository_test.go b/github/migrate_github_repository_test.go deleted file mode 100644 index 5c416889db..0000000000 --- a/github/migrate_github_repository_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package github - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestMigrateGithubRepositoryStateV0toV1(t *testing.T) { - oldAttributes := map[string]string{ - "branches.#": "1", - "branches.0.name": "foobar", - "branches.0.protected": "false", - } - - newState, err := migrateGithubRepositoryStateV0toV1(&terraform.InstanceState{ - ID: "nonempty", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{} - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } -} diff --git a/github/migrate_github_repository_webhook.go b/github/migrate_github_repository_webhook.go deleted file mode 100644 index 5379928dec..0000000000 --- a/github/migrate_github_repository_webhook.go +++ /dev/null @@ -1,54 +0,0 @@ -package github - -import ( - "fmt" - "log" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func resourceGithubWebhookMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Webhook State v0; migrating to v1") - return migrateGithubWebhookStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) - } -} - -func migrateGithubWebhookStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Webhook Attributes before migration: %#v", is.Attributes) - - prefix := "configuration." - - delete(is.Attributes, prefix+"%") - - // Read & delete old keys - oldKeys := make(map[string]string) - for k, v := range is.Attributes { - if strings.HasPrefix(k, prefix) { - oldKeys[k] = v - - // Delete old keys - delete(is.Attributes, k) - } - } - - // Write new keys - for k, v := range oldKeys { - newKey := "configuration.0." + strings.TrimPrefix(k, prefix) - is.Attributes[newKey] = v - } - - is.Attributes[prefix+"#"] = "1" - log.Printf("[DEBUG] GitHub Webhook Attributes after State Migration: %#v", is.Attributes) - - return is, nil -} diff --git a/github/migrate_github_repository_webhook_test.go b/github/migrate_github_repository_webhook_test.go deleted file mode 100644 index 159345a010..0000000000 --- a/github/migrate_github_repository_webhook_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package github - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestMigrateGithubWebhookStateV0toV1(t *testing.T) { - oldAttributes := map[string]string{ - "configuration.%": "4", - "configuration.content_type": "form", - "configuration.insecure_ssl": "0", - "configuration.secret": "blablah", - "configuration.url": "https://google.co.uk/", - } - - newState, err := migrateGithubWebhookStateV0toV1(&terraform.InstanceState{ - ID: "nonempty", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ - "configuration.#": "1", - "configuration.0.content_type": "form", - "configuration.0.insecure_ssl": "0", - "configuration.0.secret": "blablah", - "configuration.0.url": "https://google.co.uk/", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } -} diff --git a/github/resource_github_actions_organization_secret.go b/github/resource_github_actions_organization_secret.go index b6ef18e943..becfdbbc9d 100644 --- a/github/resource_github_actions_organization_secret.go +++ b/github/resource_github_actions_organization_secret.go @@ -4,22 +4,22 @@ import ( "context" "encoding/base64" "errors" - "fmt" "log" "net/http" "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 resourceGithubActionsOrganizationSecret() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsOrganizationSecretCreateOrUpdate, - Read: resourceGithubActionsOrganizationSecretRead, - Delete: resourceGithubActionsOrganizationSecretDelete, + CreateContext: resourceGithubActionsOrganizationSecretCreateOrUpdate, + ReadContext: resourceGithubActionsOrganizationSecretRead, + DeleteContext: resourceGithubActionsOrganizationSecretDelete, Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { if err := d.Set("secret_name", d.Id()); err != nil { return nil, err } @@ -33,7 +33,13 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource { // Schema migration added in v6.7.1 to handle the addition of destroy_on_drift field // Resources created before v6.7.0 need the field populated with default value SchemaVersion: 1, - MigrateState: resourceGithubActionsOrganizationSecretMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsOrganizationSecretResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "secret_name": { @@ -98,10 +104,9 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource { } } -func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() secretName := d.Get("secret_name").(string) plaintextValue := d.Get("plaintext_value").(string) @@ -111,7 +116,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids") if visibility != "selected" && hasSelectedRepositories { - return fmt.Errorf("cannot use selected_repository_ids without visibility being set to selected") + return diag.Errorf("cannot use selected_repository_ids without visibility being set to selected") } selectedRepositoryIDs := []int64{} @@ -126,7 +131,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat keyId, publicKey, err := getOrganizationPublicKeyDetails(owner, meta) if err != nil { - return err + return diag.FromErr(err) } if encryptedText, ok := d.GetOk("encrypted_value"); ok { @@ -134,7 +139,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat } else { encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) if err != nil { - return err + return diag.FromErr(err) } encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) } @@ -150,17 +155,16 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat _, err = client.Actions.CreateOrUpdateOrgSecret(ctx, owner, eSecret) if err != nil { - return err + return diag.FromErr(err) } d.SetId(secretName) - return resourceGithubActionsOrganizationSecretRead(d, meta) + return resourceGithubActionsOrganizationSecretRead(ctx, d, meta) } -func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() secret, _, err := client.Actions.GetOrgSecret(ctx, owner, d.Id()) if err != nil { @@ -173,14 +177,14 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an return nil } } - return err + return diag.FromErr(err) } if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("visibility", secret.Visibility); err != nil { - return err + return diag.FromErr(err) } selectedRepositoryIDs := []int64{} @@ -192,7 +196,7 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an for { results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, d.Id(), opt) if err != nil { - return err + return diag.FromErr(err) } for _, repo := range results.Repositories { @@ -207,7 +211,7 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an } if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { - return err + return diag.FromErr(err) } // This is a drift detection mechanism based on timestamps. @@ -235,39 +239,39 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an // Alternative approach: set sensitive values to empty to trigger update plan // This tells Terraform that the current state is unknown and needs reconciliation if err = d.Set("encrypted_value", ""); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", ""); err != nil { - return err + return diag.FromErr(err) } log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update") } } else { // No drift detected, preserve the configured values in state if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil { - return err + return diag.FromErr(err) } } // Always update the timestamp to prevent repeated drift detection if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsOrganizationSecretDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) log.Printf("[INFO] Deleting secret: %s", d.Id()) _, err := client.Actions.DeleteOrgSecret(ctx, orgName, d.Id()) - return err + return diag.FromErr(err) } func getOrganizationPublicKeyDetails(owner string, meta any) (keyId, pkValue string, err error) { diff --git a/github/resource_github_actions_organization_secret_migration.go b/github/resource_github_actions_organization_secret_migration.go new file mode 100644 index 0000000000..37d2b72db5 --- /dev/null +++ b/github/resource_github_actions_organization_secret_migration.go @@ -0,0 +1,79 @@ +package github + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubActionsOrganizationSecretResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the secret.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "encrypted_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"plaintext_value"}, + Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", + ValidateDiagFunc: toDiagFunc(validation.StringIsBase64, "encrypted_value"), + }, + "plaintext_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"encrypted_value"}, + Description: "Plaintext value of the secret to be encrypted.", + }, + "visibility": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}), + Description: "Configures the access that repositories have to the organization secret. Must be one of 'all', 'private', or 'selected'. 'selected_repository_ids' is required if set to 'selected'.", + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + ForceNew: true, + Description: "An array of repository ids that can access the organization secret.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' update.", + }, + }, + } +} + +func resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes before migration: %#v", rawState) + // Add the destroy_on_drift field with default value true if it doesn't exist + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true + } + + log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_organization_secret_migration_test.go b/github/resource_github_actions_organization_secret_migration_test.go new file mode 100644 index 0000000000..9091bf63ea --- /dev/null +++ b/github/resource_github_actions_organization_secret_migration_test.go @@ -0,0 +1,55 @@ +package github + +import ( + "reflect" + "testing" +) + +func testResourceGithubActionsOrganizationSecretInstanceStateDataV0() map[string]any { + return map[string]any{ + "id": "test-secret", + "secret_name": "test-secret", + "visibility": "private", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "plaintext_value": "secret-value", + } +} + +func testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsOrganizationSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} + +func testResourceGithubActionsOrganizationSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsOrganizationSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} + +func TestGithub_MigrateActionsOrganizationSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsOrganizationSecretInstanceStateDataV1() + actual, err := resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsOrganizationSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) + + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/resource_github_actions_secret.go b/github/resource_github_actions_secret.go index 6788348274..7f82291a22 100644 --- a/github/resource_github_actions_secret.go +++ b/github/resource_github_actions_secret.go @@ -10,23 +10,30 @@ import ( "strings" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/crypto/nacl/box" ) func resourceGithubActionsSecret() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsSecretCreateOrUpdate, - Read: resourceGithubActionsSecretRead, - Delete: resourceGithubActionsSecretDelete, + CreateContext: resourceGithubActionsSecretCreateOrUpdate, + ReadContext: resourceGithubActionsSecretRead, + DeleteContext: resourceGithubActionsSecretDelete, Importer: &schema.ResourceImporter{ - State: resourceGithubActionsSecretImport, + StateContext: resourceGithubActionsSecretImport, }, // Schema migration added to handle the addition of destroy_on_drift field // Resources created before this field was added need it populated with default value SchemaVersion: 1, - MigrateState: resourceGithubActionsSecretMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsSecretResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsSecretInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "repository": { @@ -79,10 +86,9 @@ func resourceGithubActionsSecret() *schema.Resource { } } -func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() repo := d.Get("repository").(string) secretName := d.Get("secret_name").(string) @@ -91,7 +97,7 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) keyId, publicKey, err := getPublicKeyDetails(owner, repo, meta) if err != nil { - return err + return diag.FromErr(err) } if encryptedText, ok := d.GetOk("encrypted_value"); ok { @@ -99,7 +105,7 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) } else { encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) if err != nil { - return err + return diag.FromErr(err) } encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) } @@ -113,21 +119,20 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) _, err = client.Actions.CreateOrUpdateRepoSecret(ctx, owner, repo, eSecret) if err != nil { - return err + return diag.FromErr(err) } d.SetId(buildTwoPartID(repo, secretName)) - return resourceGithubActionsSecretRead(d, meta) + return resourceGithubActionsSecretRead(ctx, d, meta) } -func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name") if err != nil { - return err + return diag.FromErr(err) } secret, _, err := client.Actions.GetRepoSecret(ctx, owner, repoName, secretName) @@ -141,11 +146,11 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { return nil } } - return err + return diag.FromErr(err) } if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { - return err + return diag.FromErr(err) } // This is a drift detection mechanism based on timestamps. @@ -173,48 +178,47 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { // Alternative approach: set sensitive values to empty to trigger update plan // This tells Terraform that the current state is unknown and needs reconciliation if err = d.Set("encrypted_value", ""); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", ""); err != nil { - return err + return diag.FromErr(err) } log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update") } } else { // No drift detected, preserve the configured values in state if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil { - return err + return diag.FromErr(err) } } // Always update the timestamp to prevent repeated drift detection if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsSecretDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name") if err != nil { - return err + return diag.FromErr(err) } _, err = client.Actions.DeleteRepoSecret(ctx, orgName, repoName, secretName) - return err + return diag.FromErr(err) } -func resourceGithubActionsSecretImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubActionsSecretImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() parts := strings.Split(d.Id(), "/") if len(parts) != 2 { diff --git a/github/resource_github_actions_secret_migration.go b/github/resource_github_actions_secret_migration.go new file mode 100644 index 0000000000..5be1ce20bb --- /dev/null +++ b/github/resource_github_actions_secret_migration.go @@ -0,0 +1,66 @@ +package github + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubActionsSecretResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the repository.", + }, + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the secret.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "encrypted_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"plaintext_value"}, + Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", + }, + "plaintext_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"encrypted_value"}, + Description: "Plaintext value of the secret to be encrypted.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' update.", + }, + }, + } +} + +func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) + // Add the destroy_on_drift field with default value true if it doesn't exist + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true + } + + log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_secret_migration_test.go b/github/resource_github_actions_secret_migration_test.go new file mode 100644 index 0000000000..7729d0191a --- /dev/null +++ b/github/resource_github_actions_secret_migration_test.go @@ -0,0 +1,55 @@ +package github + +import ( + "reflect" + "testing" +) + +func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { + return map[string]any{ + "id": "test-secret", + "repository": "test-repo", + "secret_name": "test-secret", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "plaintext_value": "secret-value", + } +} + +func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} + +func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} + +func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV1() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) + + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/migrate_github_branch_protection.go b/github/resource_github_branch_protection_migration.go similarity index 100% rename from github/migrate_github_branch_protection.go rename to github/resource_github_branch_protection_migration.go diff --git a/github/resource_github_organization_webhook.go b/github/resource_github_organization_webhook.go index 42127b59d8..5c6dfa65e7 100644 --- a/github/resource_github_organization_webhook.go +++ b/github/resource_github_organization_webhook.go @@ -8,21 +8,28 @@ import ( "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 resourceGithubOrganizationWebhook() *schema.Resource { return &schema.Resource{ - Create: resourceGithubOrganizationWebhookCreate, - Read: resourceGithubOrganizationWebhookRead, - Update: resourceGithubOrganizationWebhookUpdate, - Delete: resourceGithubOrganizationWebhookDelete, + CreateContext: resourceGithubOrganizationWebhookCreate, + ReadContext: resourceGithubOrganizationWebhookRead, + UpdateContext: resourceGithubOrganizationWebhookUpdate, + DeleteContext: resourceGithubOrganizationWebhookDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, SchemaVersion: 1, - MigrateState: resourceGithubWebhookMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubOrganizationWebhookResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubOrganizationWebhookInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "events": { @@ -73,21 +80,20 @@ func resourceGithubOrganizationWebhookObject(d *schema.ResourceData) *github.Hoo return hook } -func resourceGithubOrganizationWebhookCreate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name webhookObj := resourceGithubOrganizationWebhookObject(d) - ctx := context.Background() hook, _, err := client.Organizations.CreateHook(ctx, orgName, webhookObj) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(hook.GetID(), 10)) @@ -99,16 +105,16 @@ func resourceGithubOrganizationWebhookCreate(d *schema.ResourceData, meta any) e } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } - return resourceGithubOrganizationWebhookRead(d, meta) + return resourceGithubOrganizationWebhookRead(ctx, d, meta) } -func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client @@ -116,9 +122,9 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err orgName := meta.(*Owner).name hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -137,20 +143,20 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err return nil } } - return err + return diag.FromErr(err) } if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("url", hook.GetURL()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("active", hook.GetActive()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("events", hook.Events); err != nil { - return err + return diag.FromErr(err) } // GitHub returns the secret as a string of 8 astrisks "********" @@ -166,16 +172,16 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubOrganizationWebhookUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client @@ -184,23 +190,23 @@ func resourceGithubOrganizationWebhookUpdate(d *schema.ResourceData, meta any) e webhookObj := resourceGithubOrganizationWebhookObject(d) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, _, err = client.Organizations.EditHook(ctx, orgName, hookID, webhookObj) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubOrganizationWebhookRead(d, meta) + return resourceGithubOrganizationWebhookRead(ctx, d, meta) } -func resourceGithubOrganizationWebhookDelete(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client @@ -208,12 +214,12 @@ func resourceGithubOrganizationWebhookDelete(d *schema.ResourceData, meta any) e orgName := meta.(*Owner).name hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, err = client.Organizations.DeleteHook(ctx, orgName, hookID) - return err + return diag.FromErr(err) } func webhookConfigFromInterface(config map[string]any) *github.HookConfig { diff --git a/github/resource_github_organization_webhook_migration.go b/github/resource_github_organization_webhook_migration.go new file mode 100644 index 0000000000..b7d86e55a7 --- /dev/null +++ b/github/resource_github_organization_webhook_migration.go @@ -0,0 +1,70 @@ +package github + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubOrganizationWebhookResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "events": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "configuration": { + Type: schema.TypeMap, + Optional: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceGithubOrganizationWebhookInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Organization Webhook State before migration: %#v", rawState) + + prefix := "configuration." + delete(rawState, prefix+"%") + + // Read & delete old keys + oldKeys := make(map[string]any) + for k, v := range rawState { + if strings.HasPrefix(k, prefix) { + oldKeys[k] = v + + // Delete old keys + delete(rawState, k) + } + } + + // Write new keys + for k, v := range oldKeys { + newKey := "configuration.0." + strings.TrimPrefix(k, prefix) + rawState[newKey] = v + } + + rawState[prefix+"#"] = "1" + + log.Printf("[DEBUG] GitHub Organization Webhook State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_organization_webhook_migration_test.go b/github/resource_github_organization_webhook_migration_test.go new file mode 100644 index 0000000000..481d16a0ee --- /dev/null +++ b/github/resource_github_organization_webhook_migration_test.go @@ -0,0 +1,20 @@ +package github + +import ( + "reflect" + "testing" +) + +func TestGithub_MigrateOrganizationWebhookStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubWebhookInstanceStateDataV1() + actual, err := resourceGithubOrganizationWebhookInstanceStateUpgradeV0(t.Context(), testResourceGithubWebhookInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 4d69afbb15..9540f83eff 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -32,7 +32,13 @@ func resourceGithubRepository() *schema.Resource { }, SchemaVersion: 1, - MigrateState: resourceGithubRepositoryMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "name": { diff --git a/github/resource_github_repository_migration.go b/github/resource_github_repository_migration.go new file mode 100644 index 0000000000..a5a570461d --- /dev/null +++ b/github/resource_github_repository_migration.go @@ -0,0 +1,216 @@ +package github + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubRepositoryResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "full_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"full_name"}, + }, + "only_protected_branches": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "description": { + Type: schema.TypeString, + Default: nil, + Optional: true, + }, + "homepage_url": { + Type: schema.TypeString, + Default: "", + Optional: true, + }, + "private": { + Type: schema.TypeBool, + Computed: true, + }, + "visibility": { + Type: schema.TypeString, + Computed: true, + }, + "has_issues": { + Type: schema.TypeBool, + Computed: true, + }, + "has_projects": { + Type: schema.TypeBool, + Computed: true, + }, + "has_downloads": { + Type: schema.TypeBool, + Computed: true, + }, + "has_wiki": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_merge_commit": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_squash_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_rebase_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_auto_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "squash_merge_commit_title": { + Type: schema.TypeString, + Computed: true, + }, + "squash_merge_commit_message": { + Type: schema.TypeString, + Computed: true, + }, + "merge_commit_title": { + Type: schema.TypeString, + Computed: true, + }, + "merge_commit_message": { + Type: schema.TypeString, + Computed: true, + }, + "default_branch": { + Type: schema.TypeString, + Computed: true, + }, + "archived": { + Type: schema.TypeBool, + Computed: true, + }, + "branches": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "protected": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "pages": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch": { + Type: schema.TypeString, + Computed: true, + }, + "path": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "cname": { + Type: schema.TypeString, + Computed: true, + }, + "custom_404": { + Type: schema.TypeBool, + Computed: true, + }, + "html_url": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "topics": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "html_url": { + Type: schema.TypeString, + Computed: true, + }, + "ssh_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "svn_url": { + Type: schema.TypeString, + Computed: true, + }, + "git_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "http_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "node_id": { + Type: schema.TypeString, + Computed: true, + }, + "repo_id": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceGithubRepositoryInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Repository State before migration: %#v", rawState) + + prefix := "branches." + + for k := range rawState { + if strings.HasPrefix(k, prefix) { + delete(rawState, k) + } + } + + log.Printf("[DEBUG] GitHub Repository State after migration: %#v", rawState) + return rawState, nil +} diff --git a/github/resource_github_repository_migration_test.go b/github/resource_github_repository_migration_test.go new file mode 100644 index 0000000000..2d7effb10a --- /dev/null +++ b/github/resource_github_repository_migration_test.go @@ -0,0 +1,31 @@ +package github + +import ( + "reflect" + "testing" +) + +func testResourceGithubRepositoryInstanceStateDataV0() map[string]any { + return map[string]any{ + "branches.#": "1", + "branches.0.name": "foobar", + "branches.0.protected": "false", + } +} + +func testResourceGithubRepositoryInstanceStateDataV1() map[string]any { + return map[string]any{} +} + +func TestGithub_MigrateRepositoryStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubRepositoryInstanceStateDataV1() + actual, err := resourceGithubRepositoryInstanceStateUpgradeV0(t.Context(), testResourceGithubRepositoryInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/resource_github_repository_webhook.go b/github/resource_github_repository_webhook.go index fbd7dc274d..db105e35ae 100644 --- a/github/resource_github_repository_webhook.go +++ b/github/resource_github_repository_webhook.go @@ -10,17 +10,18 @@ import ( "strings" "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 resourceGithubRepositoryWebhook() *schema.Resource { return &schema.Resource{ - Create: resourceGithubRepositoryWebhookCreate, - Read: resourceGithubRepositoryWebhookRead, - Update: resourceGithubRepositoryWebhookUpdate, - Delete: resourceGithubRepositoryWebhookDelete, + CreateContext: resourceGithubRepositoryWebhookCreate, + ReadContext: resourceGithubRepositoryWebhookRead, + UpdateContext: resourceGithubRepositoryWebhookUpdate, + DeleteContext: resourceGithubRepositoryWebhookDelete, Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { parts := strings.Split(d.Id(), "/") if len(parts) != 2 { return nil, fmt.Errorf("invalid ID specified: supplied ID must be written as /") @@ -34,7 +35,13 @@ func resourceGithubRepositoryWebhook() *schema.Resource { }, SchemaVersion: 1, - MigrateState: resourceGithubWebhookMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryWebhookResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryWebhookInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "repository": { @@ -98,17 +105,16 @@ func resourceGithubRepositoryWebhookObject(d *schema.ResourceData) *github.Hook return hook } -func resourceGithubRepositoryWebhookCreate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hk := resourceGithubRepositoryWebhookObject(d) - ctx := context.Background() hook, _, err := client.Repositories.CreateHook(ctx, owner, repoName, hk) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(hook.GetID(), 10)) @@ -120,22 +126,22 @@ func resourceGithubRepositoryWebhookCreate(d *schema.ResourceData, meta any) err } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } - return resourceGithubRepositoryWebhookRead(d, meta) + return resourceGithubRepositoryWebhookRead(ctx, d, meta) } -func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -154,16 +160,16 @@ func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error return nil } } - return err + return diag.FromErr(err) } if err = d.Set("url", hook.GetURL()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("active", hook.GetActive()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("events", hook.Events); err != nil { - return err + return diag.FromErr(err) } // GitHub returns the secret as a string of 8 astrisks "********" @@ -179,13 +185,13 @@ func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubRepositoryWebhookUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name @@ -193,29 +199,29 @@ func resourceGithubRepositoryWebhookUpdate(d *schema.ResourceData, meta any) err hk := resourceGithubRepositoryWebhookObject(d) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, _, err = client.Repositories.EditHook(ctx, owner, repoName, hookID, hk) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubRepositoryWebhookRead(d, meta) + return resourceGithubRepositoryWebhookRead(ctx, d, meta) } -func resourceGithubRepositoryWebhookDelete(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, err = client.Repositories.DeleteHook(ctx, owner, repoName, hookID) - return handleArchivedRepoDelete(err, "repository webhook", d.Id(), owner, repoName) + return diag.FromErr(handleArchivedRepoDelete(err, "repository webhook", d.Id(), owner, repoName)) } diff --git a/github/resource_github_repository_webhook_migration.go b/github/resource_github_repository_webhook_migration.go new file mode 100644 index 0000000000..53ec661701 --- /dev/null +++ b/github/resource_github_repository_webhook_migration.go @@ -0,0 +1,75 @@ +package github + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubRepositoryWebhookResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "events": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "configuration": { + Type: schema.TypeMap, + Optional: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceGithubRepositoryWebhookInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Repository Webhook State before migration: %#v", rawState) + + prefix := "configuration." + delete(rawState, prefix+"%") + + // Read & delete old keys + oldKeys := make(map[string]any) + for k, v := range rawState { + if strings.HasPrefix(k, prefix) { + oldKeys[k] = v + + // Delete old keys + delete(rawState, k) + } + } + + // Write new keys + for k, v := range oldKeys { + newKey := "configuration.0." + strings.TrimPrefix(k, prefix) + rawState[newKey] = v + } + + rawState[prefix+"#"] = "1" + + log.Printf("[DEBUG] GitHub Repository Webhook State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_repository_webhook_migration_test.go b/github/resource_github_repository_webhook_migration_test.go new file mode 100644 index 0000000000..876405c253 --- /dev/null +++ b/github/resource_github_repository_webhook_migration_test.go @@ -0,0 +1,41 @@ +package github + +import ( + "reflect" + "testing" +) + +func testResourceGithubWebhookInstanceStateDataV0() map[string]any { + return map[string]any{ + "configuration.%": "4", + "configuration.content_type": "form", + "configuration.insecure_ssl": "0", + "configuration.secret": "blablah", + "configuration.url": "https://google.co.uk/", + } +} + +func testResourceGithubWebhookInstanceStateDataV1() map[string]any { + v0 := testResourceGithubWebhookInstanceStateDataV0() + return map[string]any{ + "configuration.#": "1", + "configuration.0.content_type": v0["configuration.content_type"], + "configuration.0.insecure_ssl": v0["configuration.insecure_ssl"], + "configuration.0.secret": v0["configuration.secret"], + "configuration.0.url": v0["configuration.url"], + } +} + +func TestGithub_MigrateRepositoryWebhookStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubWebhookInstanceStateDataV1() + actual, err := resourceGithubRepositoryWebhookInstanceStateUpgradeV0(t.Context(), testResourceGithubWebhookInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +}