Skip to content

Commit 8cda607

Browse files
authored
fix: Add destroy-on-drift property to the GitHub Action Secret resource schema (#2832)
* Add destroy-on-drift property to the GitHub Action Secret resource schema * Add schema migration * Add docs for destroy_on_drift * lint fixes * Terraform requires update on calculated fields so implented update and pointed at the create func * Fixes merge drift issues with both org secrets and actions secrets
1 parent b2b85ab commit 8cda607

7 files changed

+534
-14
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
8+
)
9+
10+
func resourceGithubActionsSecretMigrateState(v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
11+
switch v {
12+
case 0:
13+
log.Printf("[INFO] Found GitHub Actions Secret State v0; migrating to v1")
14+
return migrateGithubActionsSecretStateV0toV1(is)
15+
default:
16+
return is, fmt.Errorf("unexpected schema version: %d", v)
17+
}
18+
}
19+
20+
func migrateGithubActionsSecretStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
21+
if is.Empty() {
22+
log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.")
23+
return is, nil
24+
}
25+
26+
log.Printf("[DEBUG] GitHub Actions Secret Attributes before migration: %#v", is.Attributes)
27+
28+
// Add the destroy_on_drift field with default value true if it doesn't exist
29+
if _, ok := is.Attributes["destroy_on_drift"]; !ok {
30+
is.Attributes["destroy_on_drift"] = "true"
31+
}
32+
33+
log.Printf("[DEBUG] GitHub Actions Secret Attributes after State Migration: %#v", is.Attributes)
34+
35+
return is, nil
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package github
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
8+
)
9+
10+
func TestMigrateGithubActionsSecretStateV0toV1(t *testing.T) {
11+
// Secret without destroy_on_drift should get default value
12+
oldAttributes := map[string]string{
13+
"id": "test-secret",
14+
"repository": "test-repo",
15+
"secret_name": "test-secret",
16+
"created_at": "2023-01-01T00:00:00Z",
17+
"updated_at": "2023-01-01T00:00:00Z",
18+
"plaintext_value": "secret-value",
19+
}
20+
21+
newState, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{
22+
ID: "test-secret",
23+
Attributes: oldAttributes,
24+
})
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
29+
expectedAttributes := map[string]string{
30+
"id": "test-secret",
31+
"repository": "test-repo",
32+
"secret_name": "test-secret",
33+
"created_at": "2023-01-01T00:00:00Z",
34+
"updated_at": "2023-01-01T00:00:00Z",
35+
"plaintext_value": "secret-value",
36+
"destroy_on_drift": "true",
37+
}
38+
if !reflect.DeepEqual(newState.Attributes, expectedAttributes) {
39+
t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n",
40+
expectedAttributes, newState.Attributes)
41+
}
42+
43+
// Secret with existing destroy_on_drift should be preserved
44+
oldAttributesWithDrift := map[string]string{
45+
"id": "test-secret",
46+
"repository": "test-repo",
47+
"secret_name": "test-secret",
48+
"destroy_on_drift": "false",
49+
}
50+
51+
newState2, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{
52+
ID: "test-secret",
53+
Attributes: oldAttributesWithDrift,
54+
})
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
59+
expectedAttributesWithDrift := map[string]string{
60+
"id": "test-secret",
61+
"repository": "test-repo",
62+
"secret_name": "test-secret",
63+
"destroy_on_drift": "false",
64+
}
65+
if !reflect.DeepEqual(newState2.Attributes, expectedAttributesWithDrift) {
66+
t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n",
67+
expectedAttributesWithDrift, newState2.Attributes)
68+
}
69+
}

github/resource_github_actions_organization_secret.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta in
228228
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
229229
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
230230
d.SetId("")
231-
} else if !ok {
232-
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
233-
return err
234-
}
231+
}
232+
233+
// Always update the timestamp to prevent repeated drift detection
234+
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
235+
return err
235236
}
236237

237238
return nil
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package github
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
7+
)
8+
9+
// Test for the organization secret drift detection fix
10+
func TestGithubActionsOrganizationSecretDriftDetectionFix(t *testing.T) {
11+
t.Run("always updates timestamp regardless of drift detection", func(t *testing.T) {
12+
// This test verifies the fix for the issue where updated_at was not
13+
// being set when drift was detected, causing repeated drift detection
14+
15+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]interface{}{
16+
"secret_name": "test-secret",
17+
"plaintext_value": "test-value",
18+
"visibility": "private",
19+
"destroy_on_drift": true,
20+
"updated_at": "2023-01-01T00:00:00Z", // Old timestamp
21+
})
22+
d.SetId("test-secret")
23+
24+
// Simulate the updated_at logic from the read function
25+
// This is what the actual GitHub API would return (newer timestamp)
26+
newTimestamp := "2023-01-01T12:00:00Z"
27+
28+
// Simulate the drift detection logic from resourceGithubActionsOrganizationSecretRead
29+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
30+
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != newTimestamp {
31+
// This would log the drift and clear the ID
32+
d.SetId("")
33+
}
34+
35+
// This is the key fix - always update the timestamp
36+
err := d.Set("updated_at", newTimestamp)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
// Verify that the timestamp was updated even though drift was detected
42+
if d.Get("updated_at").(string) != newTimestamp {
43+
t.Error("Expected updated_at to be set to new timestamp after drift detection")
44+
}
45+
46+
// Verify that the ID was cleared due to drift detection
47+
if d.Id() != "" {
48+
t.Error("Expected ID to be cleared due to drift detection with destroy_on_drift=true")
49+
}
50+
})
51+
52+
t.Run("does not clear ID when destroy_on_drift is false", func(t *testing.T) {
53+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]interface{}{
54+
"secret_name": "test-secret",
55+
"plaintext_value": "test-value",
56+
"visibility": "private",
57+
"destroy_on_drift": false,
58+
"updated_at": "2023-01-01T00:00:00Z", // Old timestamp
59+
})
60+
d.SetId("test-secret")
61+
62+
newTimestamp := "2023-01-01T12:00:00Z"
63+
64+
// Simulate the drift detection logic
65+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
66+
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != newTimestamp {
67+
d.SetId("")
68+
}
69+
70+
// Always update the timestamp
71+
err := d.Set("updated_at", newTimestamp)
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
76+
// Verify that the ID was NOT cleared when destroy_on_drift=false
77+
if d.Id() != "test-secret" {
78+
t.Error("Expected ID to remain when destroy_on_drift=false")
79+
}
80+
81+
// Verify that the timestamp was still updated
82+
if d.Get("updated_at").(string) != newTimestamp {
83+
t.Error("Expected updated_at to be updated even when destroy_on_drift=false")
84+
}
85+
})
86+
}

github/resource_github_actions_secret.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ func resourceGithubActionsSecret() *schema.Resource {
1717
return &schema.Resource{
1818
Create: resourceGithubActionsSecretCreateOrUpdate,
1919
Read: resourceGithubActionsSecretRead,
20+
Update: resourceGithubActionsSecretCreateOrUpdate,
2021
Delete: resourceGithubActionsSecretDelete,
2122
Importer: &schema.ResourceImporter{
2223
State: resourceGithubActionsSecretImport,
2324
},
2425

26+
// Schema migration added to handle the addition of destroy_on_drift field
27+
// Resources created before this field was added need it populated with default value
28+
SchemaVersion: 1,
29+
MigrateState: resourceGithubActionsSecretMigrateState,
30+
2531
Schema: map[string]*schema.Schema{
2632
"repository": {
2733
Type: schema.TypeString,
@@ -62,6 +68,12 @@ func resourceGithubActionsSecret() *schema.Resource {
6268
Computed: true,
6369
Description: "Date of 'actions_secret' update.",
6470
},
71+
"destroy_on_drift": {
72+
Type: schema.TypeBool,
73+
Default: true,
74+
Optional: true,
75+
Description: "Boolean indicating whether to recreate the secret if it's modified outside of Terraform. When `true` (default), Terraform will delete and recreate the secret if it detects external changes. When `false`, Terraform will acknowledge external changes but not recreate the secret.",
76+
},
6577
},
6678
}
6779
}
@@ -155,13 +167,15 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta interface{}) e
155167
// The only solution to enforce consistency between is to mark the resource
156168
// as deleted (unset the ID) in order to fix potential drift by recreating
157169
// the resource.
158-
if updatedAt, ok := d.GetOk("updated_at"); ok && updatedAt != secret.UpdatedAt.String() {
170+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
171+
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
159172
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
160173
d.SetId("")
161-
} else if !ok {
162-
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
163-
return err
164-
}
174+
}
175+
176+
// Always update the timestamp to prevent repeated drift detection
177+
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
178+
return err
165179
}
166180

167181
return nil

0 commit comments

Comments
 (0)