Skip to content

Commit 40a77ea

Browse files
committed
fix(internal/provider): handle unknown values in tf_vars and provisioner_tags
Change TerraformVariables and ProvisionerTags fields in TemplateVersion from Go native slices ([]Variable) to types.Set, matching the existing SetNestedAttribute schema definition. Go slices cannot represent unknown Terraform values during plan, causing Value Conversion Error when these fields receive dynamic values through modules or variable interpolation. Add varsFromSet helper to centralize Set-to-slice conversion with proper null/unknown handling at all four call sites. Closes #305
1 parent 351c1e6 commit 40a77ea

3 files changed

Lines changed: 194 additions & 43 deletions

File tree

internal/provider/template_data_source_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ func TestAccTemplateDataSource(t *testing.T) {
3535
Name: types.StringValue("main"),
3636
Message: types.StringValue("Initial commit"),
3737
Directory: types.StringValue("../../integration/template-test/example-template/"),
38-
TerraformVariables: []Variable{
38+
TerraformVariables: mustVariableSet([]Variable{
3939
{
4040
Name: types.StringValue("name"),
4141
Value: types.StringValue("world"),
4242
},
43-
},
43+
}),
4444
},
4545
})
4646
require.NoError(t, err)

internal/provider/template_resource.go

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ type TemplateVersion struct {
163163
Directory types.String `tfsdk:"directory"`
164164
DirectoryHash types.String `tfsdk:"directory_hash"`
165165
Active types.Bool `tfsdk:"active"`
166-
TerraformVariables []Variable `tfsdk:"tf_vars"`
167-
ProvisionerTags []Variable `tfsdk:"provisioner_tags"`
166+
TerraformVariables types.Set `tfsdk:"tf_vars"`
167+
ProvisionerTags types.Set `tfsdk:"provisioner_tags"`
168168
}
169169

170170
type Versions []TemplateVersion
@@ -183,6 +183,34 @@ type Variable struct {
183183
Value types.String `tfsdk:"value"`
184184
}
185185

186+
// variableAttrTypes returns the attribute type map for Variable objects
187+
// used to construct types.Set values.
188+
func variableAttrTypes() map[string]attr.Type {
189+
return map[string]attr.Type{
190+
"name": types.StringType,
191+
"value": types.StringType,
192+
}
193+
}
194+
195+
// variableObjectType returns the object type used as the element type
196+
// in Set attributes for Variable.
197+
func variableObjectType() types.ObjectType {
198+
return types.ObjectType{
199+
AttrTypes: variableAttrTypes(),
200+
}
201+
}
202+
203+
// varsFromSet converts a types.Set to a slice of Variable structs.
204+
// It returns nil with no error when the set is null or unknown.
205+
func varsFromSet(ctx context.Context, varSet types.Set) ([]Variable, diag.Diagnostics) {
206+
if varSet.IsNull() || varSet.IsUnknown() {
207+
return nil, nil
208+
}
209+
var vars []Variable
210+
diags := varSet.ElementsAs(ctx, &vars, false)
211+
return vars, diags
212+
}
213+
186214
var variableNestedObject = schema.NestedAttributeObject{
187215
Attributes: map[string]schema.Attribute{
188216
"name": schema.StringAttribute{
@@ -1043,7 +1071,7 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
10431071
}
10441072
}
10451073

1046-
diag = planVersions.reconcileVersionIDs(lv, configVersions, hasActiveVersion)
1074+
diag = planVersions.reconcileVersionIDs(ctx, lv, configVersions, hasActiveVersion)
10471075
if diag.HasError() {
10481076
resp.Diagnostics.Append(diag...)
10491077
return
@@ -1199,14 +1227,22 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
11991227
tflog.Info(ctx, "discovered and parsed vars files", map[string]any{
12001228
"vars": vars,
12011229
})
1202-
for _, variable := range req.Version.TerraformVariables {
1230+
tfVarSlice, varDiags := varsFromSet(ctx, req.Version.TerraformVariables)
1231+
if varDiags.HasError() {
1232+
return nil, logs, fmt.Errorf("failed to convert terraform variables: %s", varDiags.Errors())
1233+
}
1234+
for _, variable := range tfVarSlice {
12031235
vars = append(vars, codersdk.VariableValue{
12041236
Name: variable.Name.ValueString(),
12051237
Value: variable.Value.ValueString(),
12061238
})
12071239
}
1208-
provTags := make(map[string]string, len(req.Version.ProvisionerTags))
1209-
for _, provisionerTag := range req.Version.ProvisionerTags {
1240+
provTagSlice, tagDiags := varsFromSet(ctx, req.Version.ProvisionerTags)
1241+
if tagDiags.HasError() {
1242+
return nil, logs, fmt.Errorf("failed to convert provisioner tags: %s", tagDiags.Errors())
1243+
}
1244+
provTags := make(map[string]string, len(provTagSlice))
1245+
for _, provisionerTag := range provTagSlice {
12101246
provTags[provisionerTag.Name.ValueString()] = provisionerTag.Value.ValueString()
12111247
}
12121248
tmplVerReq := codersdk.CreateTemplateVersionRequest{
@@ -1444,8 +1480,13 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
14441480
lv := make(LastVersionsByHash)
14451481
for _, version := range v {
14461482
vbh, ok := lv[version.DirectoryHash.ValueString()]
1447-
tfVars := make(map[string]string, len(version.TerraformVariables))
1448-
for _, tfVar := range version.TerraformVariables {
1483+
tfVarSlice, varDiags := varsFromSet(ctx, version.TerraformVariables)
1484+
if varDiags.HasError() {
1485+
diags.Append(varDiags...)
1486+
return diags
1487+
}
1488+
tfVars := make(map[string]string, len(tfVarSlice))
1489+
for _, tfVar := range tfVarSlice {
14491490
tfVars[tfVar.Name.ValueString()] = tfVar.Value.ValueString()
14501491
}
14511492
// Store the IDs and names of all versions with the same directory hash,
@@ -1476,7 +1517,7 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
14761517
return ps.SetKey(ctx, LastVersionsKey, lvBytes)
14771518
}
14781519

1479-
func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions, hasOneActiveVersion bool) (diag diag.Diagnostics) {
1520+
func (planVersions Versions) reconcileVersionIDs(ctx context.Context, lv LastVersionsByHash, configVersions Versions, hasOneActiveVersion bool) (diag diag.Diagnostics) {
14801521
// We remove versions that we've matched from `lv`, so make a copy for
14811522
// resolving tfvar changes at the end.
14821523
fullLv := make(LastVersionsByHash)
@@ -1532,7 +1573,7 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe
15321573
if !ok {
15331574
continue
15341575
}
1535-
if tfVariablesChanged(prevs, &planVersions[i]) {
1576+
if tfVariablesChanged(ctx, prevs, &planVersions[i]) {
15361577
planVersions[i].ID = NewUUIDUnknown()
15371578
// We could always set the name to unknown here, to generate a
15381579
// random one (this is what the Web UI currently does when
@@ -1581,15 +1622,24 @@ func versionDeactivated(prevs []PreviousTemplateVersion, planned *TemplateVersio
15811622
return false
15821623
}
15831624

1584-
func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
1625+
func tfVariablesChanged(ctx context.Context, prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
15851626
for _, prev := range prevs {
15861627
if prev.ID == planned.ID.ValueUUID() {
15871628
// If the previous version has no TFVars, then it was created using
15881629
// an older provider version.
15891630
if prev.TFVars == nil {
15901631
return true
15911632
}
1592-
for _, tfVar := range planned.TerraformVariables {
1633+
plannedVars, diags := varsFromSet(ctx, planned.TerraformVariables)
1634+
if diags.HasError() {
1635+
return true
1636+
}
1637+
// If the set is unknown or null, we cannot compare and
1638+
// must treat it as changed.
1639+
if planned.TerraformVariables.IsUnknown() || planned.TerraformVariables.IsNull() {
1640+
return true
1641+
}
1642+
for _, tfVar := range plannedVars {
15931643
if prev.TFVars[tfVar.Name.ValueString()] != tfVar.Value.ValueString() {
15941644
return true
15951645
}
@@ -1598,7 +1648,6 @@ func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersio
15981648
}
15991649
}
16001650
return true
1601-
16021651
}
16031652

16041653
func formatLogs(err error, logs []codersdk.ProvisionerJobLog) string {

0 commit comments

Comments
 (0)