@@ -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
1617func 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