@@ -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,227 @@ func convertToToolVersionArchitectures(ctx context.Context, archs types.Set) ([]
222
225
223
226
return result , nil
224
227
}
228
+
229
+ // PreserveAMD64ArchsOnURLChange 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 PreserveAMD64ArchsOnURLChange () 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 () {
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
+ // If no state archs, nothing to preserve
276
+ if stateArchs .IsNull () {
277
+ return
278
+ }
279
+
280
+ var configArchsList []ToolArchitecture
281
+ configArchs := req .ConfigValue
282
+ configHasArchs := ! configArchs .IsNull () && ! configArchs .IsUnknown ()
283
+ if configHasArchs {
284
+ diags := configArchs .ElementsAs (ctx , & configArchsList , false )
285
+ if diags .HasError () {
286
+ tflog .Debug (ctx , "Error extracting config architectures" , map [string ]interface {}{
287
+ "diagnostics" : diags ,
288
+ })
289
+ return
290
+ }
291
+ }
292
+
293
+ // IMPORTANT: Check if AMD64 was intentionally removed in config
294
+ if configHasArchs {
295
+ var configArchsList []ToolArchitecture
296
+ diags := configArchs .ElementsAs (ctx , & configArchsList , false )
297
+ if diags .HasError () {
298
+ tflog .Debug (ctx , "Error extracting config architectures" , map [string ]interface {}{
299
+ "diagnostics" : diags ,
300
+ })
301
+ return
302
+ }
303
+
304
+ // Check each arch in config to see if AMD64 is there
305
+ foundAMD64 := false
306
+ for _ , arch := range configArchsList {
307
+ if arch .Arch .ValueString () == "amd64" {
308
+ foundAMD64 = true
309
+ break
310
+ }
311
+ }
312
+
313
+ // If AMD64 is explicitly NOT in config, don't add it back
314
+ if ! foundAMD64 {
315
+ tflog .Debug (ctx , "AMD64 arch explicitly removed in config, not preserving" )
316
+ return
317
+ }
318
+ }
319
+
320
+
321
+ // Extract archs from state
322
+ var stateArchsList []ToolArchitecture
323
+ diags := stateArchs .ElementsAs (ctx , & stateArchsList , false )
324
+ if diags .HasError () {
325
+ return
326
+ }
327
+
328
+ // Extract AMD64 arch from state
329
+ var amd64Arch * ToolArchitecture
330
+ for _ , arch := range stateArchsList {
331
+ if arch .Arch .ValueString () == "amd64" {
332
+ tmpArch := arch
333
+ amd64Arch = & tmpArch
334
+ break
335
+ }
336
+ }
337
+
338
+ // If no AMD64 in state, nothing to preserve
339
+ if amd64Arch == nil {
340
+ return
341
+ }
342
+
343
+ // If we got here, we need to preserve AMD64
344
+ // Add the AMD64 architecture from the state to the plan if it's not already present
345
+ var planArchsList []ToolArchitecture
346
+ diags = planArchs .ElementsAs (ctx , & planArchsList , false )
347
+ if diags .HasError () {
348
+ tflog .Debug (ctx , "Error extracting plan architectures" , map [string ]interface {}{
349
+ "diagnostics" : diags ,
350
+ })
351
+ return
352
+ }
353
+
354
+ // Check if AMD64 is already in the plan
355
+ foundAMD64 := false
356
+ for _ , arch := range planArchsList {
357
+ if arch .Arch .ValueString () == "amd64" {
358
+ foundAMD64 = true
359
+ break
360
+ }
361
+ }
362
+
363
+ // If AMD64 is not in the plan, add it
364
+ if ! foundAMD64 {
365
+ planArchsList = append (planArchsList , * amd64Arch )
366
+ newPlanArchs := ToolArchitecturesToSet (planArchsList )
367
+ resp .PlanValue = newPlanArchs
368
+ }
369
+ }
370
+
371
+ func ToolArchitecturesToSet (archs []ToolArchitecture ) types.Set {
372
+ archObjectType := ObjectTypeForArchitectures ()
373
+ attrValues := make ([]attr.Value , len (archs ))
374
+
375
+ for i , arch := range archs {
376
+ attrValues [i ] = types .ObjectValueMust (
377
+ archObjectType .AttrTypes ,
378
+ map [string ]attr.Value {
379
+ "url" : arch .URL ,
380
+ "sha" : arch .Sha ,
381
+ "os" : arch .OS ,
382
+ "arch" : arch .Arch ,
383
+ },
384
+ )
385
+ }
386
+
387
+ return types .SetValueMust (archObjectType , attrValues )
388
+ }
389
+
390
+ // ValidateToolVersion provides common validation for tool version resources
391
+ func ValidateToolVersion (ctx context.Context , url , sha types.String , archs types.Set , resourceType string ) diag.Diagnostics {
392
+ var diags diag.Diagnostics
393
+
394
+ urlPresent := ! url .IsNull () && ! url .IsUnknown ()
395
+ shaPresent := ! sha .IsNull () && ! sha .IsUnknown ()
396
+
397
+ // If URL or SHA is not set, we will rely on the archs attribute
398
+ if ! urlPresent || ! shaPresent {
399
+ return diags
400
+ }
401
+
402
+ // Check if archs is present
403
+ if ! archs .IsNull () && ! archs .IsUnknown () {
404
+ // Extract archs
405
+ var archsList []ToolArchitecture
406
+ archDiags := archs .ElementsAs (ctx , & archsList , false )
407
+ if archDiags .HasError () {
408
+ diags .Append (archDiags ... )
409
+ return diags
410
+ }
411
+
412
+ // Check for AMD64 architecture
413
+ var hasAMD64 bool
414
+ for _ , arch := range archsList {
415
+ if arch .Arch .ValueString () == "amd64" {
416
+ hasAMD64 = true
417
+
418
+ // If URL and SHA are set at top level, check they match AMD64 arch
419
+ // Check URL matches
420
+ if urlPresent && url .ValueString () != arch .URL .ValueString () {
421
+ diags .AddError (
422
+ fmt .Sprintf ("Inconsistent %s URL values" , resourceType ),
423
+ fmt .Sprintf ("Top-level URL (%s) doesn't match AMD64 architecture URL (%s)" ,
424
+ url .ValueString (), arch .URL .ValueString ()),
425
+ )
426
+ }
427
+
428
+ // Check SHA matches
429
+ if shaPresent && sha .ValueString () != arch .Sha .ValueString () {
430
+ diags .AddError (
431
+ fmt .Sprintf ("Inconsistent %s SHA values" , resourceType ),
432
+ fmt .Sprintf ("Top-level SHA (%s) doesn't match AMD64 architecture SHA (%s)" ,
433
+ sha .ValueString (), arch .Sha .ValueString ()),
434
+ )
435
+ }
436
+
437
+ break
438
+ }
439
+ }
440
+
441
+ // If top-level URL/SHA are set and no AMD64 arch found, add error
442
+ if ! hasAMD64 && (! url .IsNull () || ! sha .IsNull ()) {
443
+ diags .AddError (
444
+ fmt .Sprintf ("Missing AMD64 architecture in %s" , resourceType ),
445
+ "When specifying both top-level URL/SHA and archs, an AMD64 architecture entry must be included" ,
446
+ )
447
+ }
448
+ }
449
+
450
+ return diags
451
+ }
0 commit comments