Skip to content

Commit d8a4694

Browse files
committed
fix: Correct secret drift implementation
Signed-off-by: Steve Hipwell <[email protected]>
1 parent e3a3c6c commit d8a4694

8 files changed

Lines changed: 658 additions & 513 deletions

github/acc_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,21 @@ type testAccConfig struct {
7474

7575
var testAccConf *testAccConfig
7676

77+
var testAccProviders map[string]*schema.Provider = map[string]*schema.Provider{
78+
"github": Provider(),
79+
}
80+
7781
// providerFactories are used to instantiate a provider during acceptance testing.
7882
// The factory function will be invoked for every Terraform CLI command executed
7983
// to create a provider server to which the CLI can reattach.
80-
var providerFactories = map[string]func() (*schema.Provider, error){
81-
//nolint:unparam
82-
"github": func() (*schema.Provider, error) {
83-
return Provider(), nil
84-
},
85-
}
84+
var (
85+
providerFactories = map[string]func() (*schema.Provider, error){
86+
//nolint:unparam
87+
"github": func() (*schema.Provider, error) {
88+
return Provider(), nil
89+
},
90+
}
91+
)
8692

8793
func TestMain(m *testing.M) {
8894
authMode := testMode(os.Getenv("GH_TEST_AUTH_MODE"))

github/provider_test.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,9 @@ import (
55
"testing"
66

77
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8-
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
98
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
109
)
1110

12-
var (
13-
testAccProviders map[string]*schema.Provider
14-
testAccProviderFactories func(providers *[]*schema.Provider) map[string]func() (*schema.Provider, error)
15-
testAccProvider *schema.Provider
16-
)
17-
18-
func init() {
19-
testAccProvider = Provider()
20-
testAccProviders = map[string]*schema.Provider{
21-
"github": testAccProvider,
22-
}
23-
testAccProviderFactories = func(providers *[]*schema.Provider) map[string]func() (*schema.Provider, error) {
24-
return map[string]func() (*schema.Provider, error){
25-
//nolint:unparam
26-
"github": func() (*schema.Provider, error) {
27-
p := Provider()
28-
*providers = append(*providers, p)
29-
return p, nil
30-
},
31-
}
32-
}
33-
}
34-
3511
func TestProvider(t *testing.T) {
3612
t.Run("runs internal validation without error", func(t *testing.T) {
3713
if err := Provider().InternalValidate(); err != nil {

github/resource_github_actions_environment_secret.go

Lines changed: 131 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,41 @@ import (
99
"net/url"
1010

1111
"github.com/google/go-github/v81/github"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1415
)
1516

1617
func resourceGithubActionsEnvironmentSecret() *schema.Resource {
1718
return &schema.Resource{
18-
Create: resourceGithubActionsEnvironmentSecretCreateOrUpdate,
19-
Read: resourceGithubActionsEnvironmentSecretRead,
20-
Delete: resourceGithubActionsEnvironmentSecretDelete,
19+
CreateContext: resourceGithubActionsEnvironmentSecretCreate,
20+
ReadContext: resourceGithubActionsEnvironmentSecretRead,
21+
DeleteContext: resourceGithubActionsEnvironmentSecretDelete,
22+
23+
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, m any) error {
24+
if len(diff.Id()) == 0 {
25+
return nil
26+
}
27+
28+
remoteUpdatedAt := diff.Get("remote_updated_at").(string)
29+
if len(remoteUpdatedAt) == 0 {
30+
return nil
31+
}
32+
33+
updatedAt := diff.Get("updated_at").(string)
34+
if updatedAt != remoteUpdatedAt {
35+
err := diff.SetNew("updated_at", remoteUpdatedAt)
36+
if err != nil {
37+
return err
38+
}
39+
40+
if len(updatedAt) != 0 {
41+
return diff.ForceNew("updated_at")
42+
}
43+
}
44+
45+
return nil
46+
},
2147

2248
Schema: map[string]*schema.Schema{
2349
"repository": {
@@ -39,6 +65,14 @@ func resourceGithubActionsEnvironmentSecret() *schema.Resource {
3965
Description: "Name of the secret.",
4066
ValidateDiagFunc: validateSecretNameFunc,
4167
},
68+
"key_id": {
69+
Type: schema.TypeString,
70+
Optional: true,
71+
Computed: true,
72+
ForceNew: true,
73+
Description: "ID of the public key used to encrypt the secret.",
74+
ConflictsWith: []string{"plaintext_value"},
75+
},
4276
"encrypted_value": {
4377
Type: schema.TypeString,
4478
Optional: true,
@@ -66,161 +100,172 @@ func resourceGithubActionsEnvironmentSecret() *schema.Resource {
66100
Computed: true,
67101
Description: "Date of 'actions_environment_secret' update.",
68102
},
103+
"remote_updated_at": {
104+
Type: schema.TypeString,
105+
Computed: true,
106+
Description: "Date of remote 'actions_environment_secret' update.",
107+
},
69108
},
70109
}
71110
}
72111

73-
func resourceGithubActionsEnvironmentSecretCreateOrUpdate(d *schema.ResourceData, meta any) error {
74-
client := meta.(*Owner).v3client
75-
owner := meta.(*Owner).name
76-
ctx := context.Background()
112+
func resourceGithubActionsEnvironmentSecretCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
113+
meta := m.(*Owner)
114+
client := meta.v3client
115+
owner := meta.name
77116

78117
repoName := d.Get("repository").(string)
79118
envName := d.Get("environment").(string)
119+
name := d.Get("secret_name").(string)
120+
keyID := d.Get("key_id").(string)
121+
encryptedValue := d.Get("encrypted_value").(string)
122+
80123
escapedEnvName := url.PathEscape(envName)
81-
secretName := d.Get("secret_name").(string)
82-
plaintextValue := d.Get("plaintext_value").(string)
83-
var encryptedValue string
84124

85125
repo, _, err := client.Repositories.Get(ctx, owner, repoName)
86126
if err != nil {
87-
return err
127+
return diag.FromErr(err)
88128
}
129+
repoID := int(repo.GetID())
89130

90-
keyId, publicKey, err := getEnvironmentPublicKeyDetails(repo.GetID(), escapedEnvName, meta)
91-
if err != nil {
92-
return err
131+
var publicKey string
132+
if len(keyID) == 0 || len(encryptedValue) == 0 {
133+
keyID, publicKey, err = getEnvironmentPublicKeyDetails(ctx, meta, repoID, escapedEnvName)
134+
if err != nil {
135+
return diag.FromErr(err)
136+
}
93137
}
94138

95-
if encryptedText, ok := d.GetOk("encrypted_value"); ok {
96-
encryptedValue = encryptedText.(string)
97-
} else {
139+
if len(encryptedValue) == 0 {
140+
plaintextValue := d.Get("plaintext_value").(string)
141+
98142
encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey)
99143
if err != nil {
100-
return err
144+
return diag.FromErr(err)
101145
}
102146
encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes)
103147
}
104148

105-
// Create an EncryptedSecret and encrypt the plaintext value into it
106-
eSecret := &github.EncryptedSecret{
107-
Name: secretName,
108-
KeyID: keyId,
149+
_, err = client.Actions.CreateOrUpdateEnvSecret(ctx, repoID, escapedEnvName, &github.EncryptedSecret{
150+
Name: name,
151+
KeyID: keyID,
109152
EncryptedValue: encryptedValue,
153+
})
154+
if err != nil {
155+
return diag.FromErr(err)
110156
}
111157

112-
_, err = client.Actions.CreateOrUpdateEnvSecret(ctx, int(repo.GetID()), escapedEnvName, eSecret)
113-
if err != nil {
114-
return err
158+
d.SetId(buildID(repoName, envName, name))
159+
if err := d.Set("key_id", keyID); err != nil {
160+
return diag.FromErr(err)
115161
}
116162

117-
d.SetId(buildThreePartID(repoName, envName, secretName))
118-
return resourceGithubActionsEnvironmentSecretRead(d, meta)
163+
// GitHub API does not return on create so we have to lookup the secret to get timestamps
164+
if secret, _, err := client.Actions.GetEnvSecret(ctx, repoID, escapedEnvName, name); err == nil {
165+
if err := d.Set("created_at", secret.CreatedAt.String()); err != nil {
166+
return diag.FromErr(err)
167+
}
168+
if err := d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
169+
return diag.FromErr(err)
170+
}
171+
if err := d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil {
172+
return diag.FromErr(err)
173+
}
174+
}
175+
176+
return nil
119177
}
120178

121-
func resourceGithubActionsEnvironmentSecretRead(d *schema.ResourceData, meta any) error {
122-
client := meta.(*Owner).v3client
123-
owner := meta.(*Owner).name
124-
ctx := context.Background()
179+
func resourceGithubActionsEnvironmentSecretRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
180+
meta := m.(*Owner)
181+
client := meta.v3client
182+
owner := meta.name
125183

126-
repoName, envName, secretName, err := parseThreePartID(d.Id(), "repository", "environment", "secret_name")
184+
repoName, envName, name, err := parseID3(d.Id())
127185
if err != nil {
128-
return err
186+
return diag.FromErr(err)
129187
}
188+
130189
escapedEnvName := url.PathEscape(envName)
131190

132191
repo, _, err := client.Repositories.Get(ctx, owner, repoName)
133192
if err != nil {
134193
var ghErr *github.ErrorResponse
135194
if errors.As(err, &ghErr) {
136195
if ghErr.Response.StatusCode == http.StatusNotFound {
137-
log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub",
138-
d.Id())
196+
log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub", d.Id())
139197
d.SetId("")
140198
return nil
141199
}
142200
}
143-
return err
201+
return diag.FromErr(err)
144202
}
145203

146-
secret, _, err := client.Actions.GetEnvSecret(ctx, int(repo.GetID()), escapedEnvName, secretName)
204+
secret, _, err := client.Actions.GetEnvSecret(ctx, int(repo.GetID()), escapedEnvName, name)
147205
if err != nil {
148206
var ghErr *github.ErrorResponse
149207
if errors.As(err, &ghErr) {
150208
if ghErr.Response.StatusCode == http.StatusNotFound {
151-
log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub",
152-
d.Id())
209+
log.Printf("[INFO] Removing environment secret %s from state because it no longer exists in GitHub", d.Id())
153210
d.SetId("")
154211
return nil
155212
}
156213
}
157-
return err
158-
}
159-
160-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
161-
return err
162-
}
163-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
164-
return err
165-
}
166-
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
167-
return err
168-
}
169-
170-
// This is a drift detection mechanism based on timestamps.
171-
//
172-
// If we do not currently store the "updated_at" field, it means we've only
173-
// just created the resource and the value is most likely what we want it to
174-
// be.
175-
//
176-
// If the resource is changed externally in the meantime then reading back
177-
// the last update timestamp will return a result different than the
178-
// timestamp we've persisted in the state. In this case, we can no longer
179-
// trust that the value matches what is in the state file.
180-
//
181-
// To solve this, we must unset the values and allow Terraform to decide whether or
182-
// not this resource should be modified or left as-is (ignore_changes).
183-
if updatedAt, ok := d.GetOk("updated_at"); ok && updatedAt != secret.UpdatedAt.String() {
184-
log.Printf("[INFO] The environment secret %s has been externally updated in GitHub", d.Id())
185-
_ = d.Set("encrypted_value", "")
186-
_ = d.Set("plaintext_value", "")
187-
} else if !ok {
214+
return diag.FromErr(err)
215+
}
216+
217+
if len(d.Get("created_at").(string)) == 0 {
218+
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
219+
return diag.FromErr(err)
220+
}
221+
}
222+
223+
if len(d.Get("updated_at").(string)) == 0 {
188224
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
189-
return err
225+
return diag.FromErr(err)
190226
}
191227
}
192228

229+
if err = d.Set("remote_updated_at", secret.UpdatedAt.String()); err != nil {
230+
return diag.FromErr(err)
231+
}
232+
193233
return nil
194234
}
195235

196-
func resourceGithubActionsEnvironmentSecretDelete(d *schema.ResourceData, meta any) error {
197-
client := meta.(*Owner).v3client
198-
owner := meta.(*Owner).name
199-
ctx := context.WithValue(context.Background(), ctxId, d.Id())
236+
func resourceGithubActionsEnvironmentSecretDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
237+
meta := m.(*Owner)
238+
client := meta.v3client
239+
owner := meta.name
200240

201-
repoName, envName, secretName, err := parseThreePartID(d.Id(), "repository", "environment", "secret_name")
241+
repoName, envName, name, err := parseID3(d.Id())
202242
if err != nil {
203-
return err
243+
return diag.FromErr(err)
204244
}
245+
205246
escapedEnvName := url.PathEscape(envName)
247+
206248
repo, _, err := client.Repositories.Get(ctx, owner, repoName)
207249
if err != nil {
208-
return err
250+
return diag.FromErr(err)
209251
}
252+
210253
log.Printf("[INFO] Deleting environment secret: %s", d.Id())
211-
_, err = client.Actions.DeleteEnvSecret(ctx, int(repo.GetID()), escapedEnvName, secretName)
254+
_, err = client.Actions.DeleteEnvSecret(ctx, int(repo.GetID()), escapedEnvName, name)
255+
if err != nil {
256+
return diag.FromErr(err)
257+
}
212258

213-
return err
259+
return nil
214260
}
215261

216-
func getEnvironmentPublicKeyDetails(repoID int64, envName string, meta any) (keyId, pkValue string, err error) {
217-
client := meta.(*Owner).v3client
218-
ctx := context.Background()
262+
func getEnvironmentPublicKeyDetails(ctx context.Context, meta *Owner, repoID int, envName string) (string, string, error) {
263+
client := meta.v3client
219264

220-
publicKey, _, err := client.Actions.GetEnvPublicKey(ctx, int(repoID), envName)
265+
publicKey, _, err := client.Actions.GetEnvPublicKey(ctx, repoID, envName)
221266
if err != nil {
222-
return keyId, pkValue, err
267+
return "", "", err
223268
}
224269

225-
return publicKey.GetKeyID(), publicKey.GetKey(), err
270+
return publicKey.GetKeyID(), publicKey.GetKey(), nil
226271
}

0 commit comments

Comments
 (0)