Skip to content

Commit a07f685

Browse files
committed
currently works
1 parent 8b0ccdd commit a07f685

File tree

3 files changed

+250
-17
lines changed

3 files changed

+250
-17
lines changed

internal/provider/resource_tfe_opa_version.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
// Copyright (c) HashiCorp, Inc.
22
// SPDX-License-Identifier: MPL-2.0
33

4-
// NOTE: This is a legacy resource and should be migrated to the Plugin
5-
// Framework if substantial modifications are planned. See
6-
// docs/new-resources.md if planning to use this code as boilerplate for
7-
// a new resource.
8-
94
package provider
105

116
import (
@@ -26,9 +21,10 @@ import (
2621
)
2722

2823
var (
29-
_ resource.Resource = &OPAVersionResource{}
30-
_ resource.ResourceWithConfigure = &OPAVersionResource{}
31-
_ resource.ResourceWithImportState = &OPAVersionResource{}
24+
_ resource.Resource = &OPAVersionResource{}
25+
_ resource.ResourceWithConfigure = &OPAVersionResource{}
26+
_ resource.ResourceWithImportState = &OPAVersionResource{}
27+
_ resource.ResourceWithValidateConfig = &OPAVersionResource{} // Add this line
3228
)
3329

3430
type OPAVersionResource struct {
@@ -114,9 +110,12 @@ func (r *OPAVersionResource) Schema(ctx context.Context, req resource.SchemaRequ
114110
},
115111
},
116112
},
117-
Computed: true,
118-
Optional: true,
119-
PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()},
113+
Computed: true,
114+
Optional: true,
115+
PlanModifiers: []planmodifier.Set{
116+
PreserveAMD64ArchsOnURLChange(),
117+
setplanmodifier.UseStateForUnknown(), // This ensures that we don't show a warning for invisible changes in updates when using refresh-only mode
118+
},
120119
},
121120
},
122121
}
@@ -284,7 +283,7 @@ func (r *OPAVersionResource) Update(ctx context.Context, req resource.UpdateRequ
284283
opts := tfe.AdminOPAVersionUpdateOptions{
285284
Version: tfe.String(opaVersion.Version.ValueString()),
286285
URL: stringOrNil(opaVersion.URL.ValueString()),
287-
SHA: tfe.String(opaVersion.SHA.ValueString()),
286+
SHA: stringOrNil(opaVersion.SHA.ValueString()),
288287
Official: tfe.Bool(opaVersion.Official.ValueBool()),
289288
Enabled: tfe.Bool(opaVersion.Enabled.ValueBool()),
290289
Beta: tfe.Bool(opaVersion.Beta.ValueBool()),
@@ -391,3 +390,15 @@ func (r *OPAVersionResource) ImportState(ctx context.Context, req resource.Impor
391390
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), versionID)...)
392391
}
393392
}
393+
394+
// Make OPAVersionResource implement the ToolVersionValidator interface
395+
func (r *OPAVersionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
396+
var config modelAdminOPAVersion
397+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
398+
if resp.Diagnostics.HasError() {
399+
return
400+
}
401+
402+
// Use the simplified validation function
403+
resp.Diagnostics.Append(ValidateToolVersion(ctx, config.URL, config.SHA, config.Archs, "OPA Version")...)
404+
}

internal/provider/resource_tfe_terraform_version.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
// Copyright (c) HashiCorp, Inc.
22
// SPDX-License-Identifier: MPL-2.0
33

4-
// NOTE: This is a legacy resource and should be migrated to the Plugin
5-
// Framework if substantial modifications are planned. See
6-
// docs/new-resources.md if planning to use this code as boilerplate for
7-
// a new resource.
8-
94
package provider
105

116
import (

internal/provider/tool_helpers.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99

1010
"github.com/hashicorp/terraform-plugin-framework/attr"
1111
"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"
1214
"github.com/hashicorp/terraform-plugin-framework/types"
15+
"github.com/hashicorp/terraform-plugin-log/tflog"
1316

1417
tfe "github.com/hashicorp/go-tfe"
1518
)
@@ -222,3 +225,227 @@ func convertToToolVersionArchitectures(ctx context.Context, archs types.Set) ([]
222225

223226
return result, nil
224227
}
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

Comments
 (0)