@@ -9,7 +9,10 @@ import (
9
9
10
10
"github.com/hashicorp/terraform-plugin-framework/attr"
11
11
"github.com/hashicorp/terraform-plugin-framework/diag"
12
+ "github.com/hashicorp/terraform-plugin-framework/path"
13
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
12
14
"github.com/hashicorp/terraform-plugin-framework/types"
15
+ "github.com/hashicorp/terraform-plugin-log/tflog"
13
16
14
17
tfe "github.com/hashicorp/go-tfe"
15
18
)
@@ -222,3 +225,164 @@ func convertToToolVersionArchitectures(ctx context.Context, archs types.Set) ([]
222
225
223
226
return result , nil
224
227
}
228
+
229
+ // PreserveAMD64ArchsOnChange creates a plan modifier that preserves AMD64 architecture entries
230
+ // when top-level URL or SHA changes, to be used across all tool version resources
231
+ func PreserveAMD64ArchsOnChange () planmodifier.Set {
232
+ return & preserveAMD64ArchsModifier {}
233
+ }
234
+
235
+ // Implement the plan modifier interface
236
+ type preserveAMD64ArchsModifier struct {}
237
+
238
+ // Description provides a plain text description of the plan modifier
239
+ func (m * preserveAMD64ArchsModifier ) Description (ctx context.Context ) string {
240
+ return "Preserves AMD64 architecture entries when top-level URL or SHA changes"
241
+ }
242
+
243
+ // MarkdownDescription provides markdown documentation
244
+ func (m * preserveAMD64ArchsModifier ) MarkdownDescription (ctx context.Context ) string {
245
+ return "Preserves AMD64 architecture entries when top-level URL or SHA changes"
246
+ }
247
+
248
+ // PlanModifySet modifies the plan to ensure AMD64 architecture entries are preserved
249
+ func (m * preserveAMD64ArchsModifier ) PlanModifySet (ctx context.Context , req planmodifier.SetRequest , resp * planmodifier.SetResponse ) {
250
+ // Skip if we're destroying the resource or no state
251
+ if req .Plan .Raw .IsNull () || req .State .Raw .IsNull () || req .StateValue .IsNull () {
252
+ return
253
+ }
254
+
255
+ var stateURL , planURL , stateSHA , planSHA types.String
256
+ // Get values from state and plan
257
+ req .State .GetAttribute (ctx , path .Root ("url" ), & stateURL )
258
+ req .Plan .GetAttribute (ctx , path .Root ("url" ), & planURL )
259
+ req .State .GetAttribute (ctx , path .Root ("sha" ), & stateSHA )
260
+ req .Plan .GetAttribute (ctx , path .Root ("sha" ), & planSHA )
261
+
262
+ // Check if values are changing
263
+ urlChanged := ! stateURL .Equal (planURL )
264
+ shaChanged := ! stateSHA .Equal (planSHA )
265
+
266
+ // If neither URL nor SHA is changing, do nothing
267
+ if ! urlChanged && ! shaChanged {
268
+ return
269
+ }
270
+
271
+ // Extract state archs and plan archs
272
+ stateArchs := req .StateValue
273
+ planArchs := req .PlanValue
274
+
275
+ // Extract archs from state
276
+ var stateArchsList []ToolArchitecture
277
+ diags := stateArchs .ElementsAs (ctx , & stateArchsList , false )
278
+ if diags .HasError () {
279
+ return
280
+ }
281
+
282
+ // we need to update the plan amd url and sha to match the top level values
283
+ var planArchsList []ToolArchitecture
284
+ diags = planArchs .ElementsAs (ctx , & planArchsList , false )
285
+ if diags .HasError () {
286
+ tflog .Debug (ctx , "Error extracting plan architectures" , map [string ]interface {}{
287
+ "diagnostics" : diags ,
288
+ })
289
+ return
290
+ }
291
+
292
+ // Check if AMD64 is already in the plan
293
+ for i , arch := range planArchsList {
294
+ if arch .Arch .ValueString () == "amd64" {
295
+ // If URL or SHA is changing, update the AMD64 arch to match
296
+ if urlChanged {
297
+ arch .URL = planURL
298
+ }
299
+ if shaChanged {
300
+ arch .Sha = planSHA
301
+ }
302
+ // Update the plan architecture list with the modified AMD64 arch
303
+ planArchsList [i ] = arch
304
+
305
+ // Update the plan with the modified AMD64 arch
306
+ archObjectType := ObjectTypeForArchitectures ()
307
+ attrValues := make ([]attr.Value , len (planArchsList ))
308
+
309
+ for i , arch := range planArchsList {
310
+ attrValues [i ] = types .ObjectValueMust (
311
+ archObjectType .AttrTypes ,
312
+ map [string ]attr.Value {
313
+ "url" : arch .URL ,
314
+ "sha" : arch .Sha ,
315
+ "os" : arch .OS ,
316
+ "arch" : arch .Arch ,
317
+ },
318
+ )
319
+ }
320
+
321
+ resp .PlanValue = types .SetValueMust (archObjectType , attrValues )
322
+ return
323
+ }
324
+ }
325
+ }
326
+
327
+ // ValidateToolVersion provides common validation for tool version resources
328
+ func ValidateToolVersion (ctx context.Context , url , sha types.String , archs types.Set , resourceType string ) diag.Diagnostics {
329
+ var diags diag.Diagnostics
330
+
331
+ urlPresent := ! url .IsNull () && ! url .IsUnknown ()
332
+ shaPresent := ! sha .IsNull () && ! sha .IsUnknown ()
333
+
334
+ // If URL or SHA is not set, we will rely on the archs attribute
335
+ if ! urlPresent || ! shaPresent {
336
+ return diags
337
+ }
338
+
339
+ // Check if archs is present
340
+ if ! archs .IsNull () && ! archs .IsUnknown () {
341
+ // Extract archs
342
+ var archsList []ToolArchitecture
343
+ archDiags := archs .ElementsAs (ctx , & archsList , false )
344
+ if archDiags .HasError () {
345
+ diags .Append (archDiags ... )
346
+ return diags
347
+ }
348
+
349
+ // Check for AMD64 architecture
350
+ var hasAMD64 bool
351
+ for _ , arch := range archsList {
352
+ if arch .Arch .ValueString () == "amd64" {
353
+ hasAMD64 = true
354
+
355
+ // If URL and SHA are set at top level, check they match AMD64 arch
356
+ // Check URL matches
357
+ if urlPresent && url .ValueString () != arch .URL .ValueString () {
358
+ diags .AddError (
359
+ fmt .Sprintf ("Inconsistent %s URL values" , resourceType ),
360
+ fmt .Sprintf ("Top-level URL (%s) doesn't match AMD64 architecture URL (%s)" ,
361
+ url .ValueString (), arch .URL .ValueString ()),
362
+ )
363
+ }
364
+
365
+ // Check SHA matches
366
+ if shaPresent && sha .ValueString () != arch .Sha .ValueString () {
367
+ diags .AddError (
368
+ fmt .Sprintf ("Inconsistent %s SHA values" , resourceType ),
369
+ fmt .Sprintf ("Top-level SHA (%s) doesn't match AMD64 architecture SHA (%s)" ,
370
+ sha .ValueString (), arch .Sha .ValueString ()),
371
+ )
372
+ }
373
+
374
+ break
375
+ }
376
+ }
377
+
378
+ // If top-level URL/SHA are set and no AMD64 arch found, add error
379
+ if ! hasAMD64 && (! url .IsNull () || ! sha .IsNull ()) {
380
+ diags .AddError (
381
+ fmt .Sprintf ("Missing AMD64 architecture in %s" , resourceType ),
382
+ "When specifying both top-level URL/SHA and archs, an AMD64 architecture entry must be included" ,
383
+ )
384
+ }
385
+ }
386
+
387
+ return diags
388
+ }
0 commit comments