Skip to content

Commit e72a4fb

Browse files
maint: Integrate v6.8.3 into main (#2902)
* fix: Add state migration for github_actions_organization_secret destroy_on_drift field This addresses the regression introduced in v6.7.0 where existing github_actions_organization_secret resources would show invalid state for the new destroy_on_drift field. Adds schema migration from v0 to v1 that sets destroy_on_drift=true for existing resources that don't have this field. Fixes #2804 * Add destroy-on-drift property to the GitHub Action Secret resource schema * Add schema migration * Fixes merge drift issues with both org secrets and actions secrets * Fix InternalValidate error: Add Update function to github_actions_secret - Fixes the upgrade issue from v6.7.1 to v6.7.2. * Add validation test to prevent regression of Update function requirement * fix: Add ForceNew to destroy_on_drift and selected_repository_ids fields * Reconcile the encrypted_value, plaintext_value in the read to unknown went dates don't match * Removes the superfluous update func now that we are calcualting stored date diff in read * test clean up * fix: Correctly boxes max_file_size as an int and addresses the schema.Set type mismatch with restricted_file_extensions * Linting fixes * Updates how we handle secrets to address provider config issues * Updates deprecated func in test * Corrects docs, tests, and adds validation to max_file_size * Update github/respository_rules_utils_test.go Co-authored-by: Steve Hipwell <steve.hipwell@gmail.com> * Update github/respository_rules_utils_test.go Co-authored-by: Steve Hipwell <steve.hipwell@gmail.com> * fix tests --------- Co-authored-by: Steve Hipwell <steve.hipwell@gmail.com>
1 parent 17e9221 commit e72a4fb

15 files changed

+347
-136
lines changed

github/resource_github_actions_organization_secret.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
1717
return &schema.Resource{
1818
Create: resourceGithubActionsOrganizationSecretCreateOrUpdate,
1919
Read: resourceGithubActionsOrganizationSecretRead,
20-
Update: resourceGithubActionsOrganizationSecretCreateOrUpdate,
2120
Delete: resourceGithubActionsOrganizationSecretDelete,
2221
Importer: &schema.ResourceImporter{
2322
State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
@@ -61,8 +60,8 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
6160
"visibility": {
6261
Type: schema.TypeString,
6362
Required: true,
64-
ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}),
6563
ForceNew: true,
64+
ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}),
6665
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'.",
6766
},
6867
"selected_repository_ids": {
@@ -72,6 +71,7 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
7271
},
7372
Set: schema.HashInt,
7473
Optional: true,
74+
ForceNew: true,
7575
Description: "An array of repository ids that can access the organization secret.",
7676
},
7777
"created_at": {
@@ -85,9 +85,11 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
8585
Description: "Date of 'actions_secret' update.",
8686
},
8787
"destroy_on_drift": {
88-
Type: schema.TypeBool,
89-
Default: true,
90-
Optional: true,
88+
Type: schema.TypeBool,
89+
Default: true,
90+
Optional: true,
91+
ForceNew: true,
92+
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.",
9193
},
9294
},
9395
}
@@ -171,12 +173,6 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an
171173
return err
172174
}
173175

174-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
175-
return err
176-
}
177-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
178-
return err
179-
}
180176
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
181177
return err
182178
}
@@ -222,14 +218,35 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an
222218
// timestamp we've persisted in the state. In that case, we can no longer
223219
// trust that the value (which we don't see) is equal to what we've declared
224220
// previously.
225-
//
226-
// The only solution to enforce consistency between is to mark the resource
227-
// as deleted (unset the ID) in order to fix potential drift by recreating
228-
// the resource.
229221
destroyOnDrift := d.Get("destroy_on_drift").(bool)
230-
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
222+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
223+
224+
if hasStoredUpdatedAt && storedUpdatedAt != secret.UpdatedAt.String() {
231225
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
232-
d.SetId("")
226+
227+
if destroyOnDrift {
228+
// Original behavior: mark for recreation
229+
d.SetId("")
230+
return nil
231+
} else {
232+
// Alternative approach: set sensitive values to empty to trigger update plan
233+
// This tells Terraform that the current state is unknown and needs reconciliation
234+
if err = d.Set("encrypted_value", ""); err != nil {
235+
return err
236+
}
237+
if err = d.Set("plaintext_value", ""); err != nil {
238+
return err
239+
}
240+
log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update")
241+
}
242+
} else {
243+
// No drift detected, preserve the configured values in state
244+
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
245+
return err
246+
}
247+
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
248+
return err
249+
}
233250
}
234251

235252
// Always update the timestamp to prevent repeated drift detection

github/resource_github_actions_organization_secret_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1011
)
1112

1213
func TestAccGithubActionsOrganizationSecret(t *testing.T) {
@@ -184,4 +185,108 @@ func TestAccGithubActionsOrganizationSecret(t *testing.T) {
184185
testCase(t, organization)
185186
})
186187
})
188+
189+
// Unit tests for drift detection behavior
190+
t.Run("destroyOnDrift false clears sensitive values instead of recreating", func(t *testing.T) {
191+
originalTimestamp := "2023-01-01T00:00:00Z"
192+
newTimestamp := "2023-01-02T00:00:00Z"
193+
194+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]any{
195+
"secret_name": "test-secret",
196+
"plaintext_value": "original-value",
197+
"encrypted_value": "original-encrypted",
198+
"visibility": "private",
199+
"destroy_on_drift": false,
200+
"updated_at": originalTimestamp,
201+
})
202+
d.SetId("test-secret")
203+
204+
// Simulate drift detection logic when destroy_on_drift is false
205+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
206+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
207+
208+
if hasStoredUpdatedAt && storedUpdatedAt != newTimestamp {
209+
if destroyOnDrift {
210+
// Would clear ID for recreation
211+
d.SetId("")
212+
} else {
213+
// Should clear sensitive values to trigger update
214+
_ = d.Set("encrypted_value", "")
215+
_ = d.Set("plaintext_value", "")
216+
}
217+
_ = d.Set("updated_at", newTimestamp)
218+
}
219+
220+
// Should NOT have cleared the ID when destroy_on_drift=false
221+
if d.Id() == "" {
222+
t.Error("Expected ID to be preserved when destroy_on_drift=false, but it was cleared")
223+
}
224+
225+
// Should have cleared sensitive values to trigger update plan
226+
if plaintextValue := d.Get("plaintext_value").(string); plaintextValue != "" {
227+
t.Errorf("Expected plaintext_value to be cleared for update plan, got %s", plaintextValue)
228+
}
229+
230+
if encryptedValue := d.Get("encrypted_value").(string); encryptedValue != "" {
231+
t.Errorf("Expected encrypted_value to be cleared for update plan, got %s", encryptedValue)
232+
}
233+
234+
// Should have updated the timestamp
235+
if updatedAt := d.Get("updated_at").(string); updatedAt != newTimestamp {
236+
t.Errorf("Expected timestamp to be updated to %s, got %s", newTimestamp, updatedAt)
237+
}
238+
})
239+
240+
t.Run("destroyOnDrift true still recreates resource on drift", func(t *testing.T) {
241+
originalTimestamp := "2023-01-01T00:00:00Z"
242+
newTimestamp := "2023-01-02T00:00:00Z"
243+
244+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]any{
245+
"secret_name": "test-secret",
246+
"plaintext_value": "original-value",
247+
"visibility": "private",
248+
"destroy_on_drift": true, // Explicitly set to true
249+
"updated_at": originalTimestamp,
250+
})
251+
d.SetId("test-secret")
252+
253+
// Simulate drift detection logic when destroy_on_drift is true
254+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
255+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
256+
257+
if hasStoredUpdatedAt && storedUpdatedAt != newTimestamp {
258+
if destroyOnDrift {
259+
// Should clear ID for recreation (original behavior)
260+
d.SetId("")
261+
return // Exit early like the real function would
262+
}
263+
}
264+
265+
// Should have cleared the ID for recreation when destroy_on_drift=true
266+
if d.Id() != "" {
267+
t.Error("Expected ID to be cleared for recreation when destroy_on_drift=true, but it was preserved")
268+
}
269+
})
270+
271+
t.Run("destroy_on_drift field defaults", func(t *testing.T) {
272+
// Test that destroy_on_drift defaults to true for backward compatibility
273+
schema := resourceGithubActionsOrganizationSecret().Schema["destroy_on_drift"]
274+
if schema.Default != true {
275+
t.Error("destroy_on_drift should default to true for backward compatibility")
276+
}
277+
})
278+
279+
t.Run("default destroy_on_drift is true", func(t *testing.T) {
280+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]any{
281+
"secret_name": "test-secret",
282+
"plaintext_value": "test-value",
283+
"visibility": "private",
284+
// destroy_on_drift not set, should default to true
285+
})
286+
287+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
288+
if !destroyOnDrift {
289+
t.Error("Expected destroy_on_drift to default to true")
290+
}
291+
})
187292
}

github/resource_github_actions_secret.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ func resourceGithubActionsSecret() *schema.Resource {
1818
return &schema.Resource{
1919
Create: resourceGithubActionsSecretCreateOrUpdate,
2020
Read: resourceGithubActionsSecretRead,
21-
Update: resourceGithubActionsSecretCreateOrUpdate,
2221
Delete: resourceGithubActionsSecretDelete,
2322
Importer: &schema.ResourceImporter{
2423
State: resourceGithubActionsSecretImport,
@@ -73,6 +72,7 @@ func resourceGithubActionsSecret() *schema.Resource {
7372
Type: schema.TypeBool,
7473
Default: true,
7574
Optional: true,
75+
ForceNew: true,
7676
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.",
7777
},
7878
},
@@ -144,12 +144,6 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error {
144144
return err
145145
}
146146

147-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
148-
return err
149-
}
150-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
151-
return err
152-
}
153147
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
154148
return err
155149
}
@@ -165,17 +159,36 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error {
165159
// timestamp we've persisted in the state. In that case, we can no longer
166160
// trust that the value (which we don't see) is equal to what we've declared
167161
// previously.
168-
//
169-
// The only solution to enforce consistency between is to mark the resource
170-
// as deleted (unset the ID) in order to fix potential drift by recreating
171-
// the resource.
172162
destroyOnDrift := d.Get("destroy_on_drift").(bool)
173-
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
163+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
164+
165+
if hasStoredUpdatedAt && storedUpdatedAt != secret.UpdatedAt.String() {
174166
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
175-
d.SetId("")
176-
}
177167

178-
// Always update the timestamp to prevent repeated drift detection
168+
if destroyOnDrift {
169+
// Original behavior: mark for recreation
170+
d.SetId("")
171+
return nil
172+
} else {
173+
// Alternative approach: set sensitive values to empty to trigger update plan
174+
// This tells Terraform that the current state is unknown and needs reconciliation
175+
if err = d.Set("encrypted_value", ""); err != nil {
176+
return err
177+
}
178+
if err = d.Set("plaintext_value", ""); err != nil {
179+
return err
180+
}
181+
log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update")
182+
}
183+
} else {
184+
// No drift detected, preserve the configured values in state
185+
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
186+
return err
187+
}
188+
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
189+
return err
190+
}
191+
} // Always update the timestamp to prevent repeated drift detection
179192
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
180193
return err
181194
}

0 commit comments

Comments
 (0)