@@ -286,7 +286,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
286286 },
287287 },
288288 "icon" : schema.StringAttribute {
289- MarkdownDescription : "Relative path or external URL that specifes an icon to be displayed in the dashboard." ,
289+ MarkdownDescription : "Relative path or external URL that specifies an icon to be displayed in the dashboard." ,
290290 Optional : true ,
291291 Computed : true ,
292292 Default : stringdefault .StaticString ("" ),
@@ -404,7 +404,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
404404 Required : true ,
405405 Validators : []validator.List {
406406 listvalidator .SizeAtLeast (1 ),
407- NewActiveVersionValidator (),
407+ NewVersionsValidator (),
408408 },
409409 NestedObject : schema.NestedAttributeObject {
410410 Attributes : map [string ]schema.Attribute {
@@ -867,24 +867,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
867867 return []resource.ConfigValidator {}
868868}
869869
870- type activeVersionValidator struct {}
870+ type versionsValidator struct {}
871871
872- func NewActiveVersionValidator () validator.List {
873- return & activeVersionValidator {}
872+ func NewVersionsValidator () validator.List {
873+ return & versionsValidator {}
874874}
875875
876876// Description implements validator.List.
877- func (a * activeVersionValidator ) Description (ctx context.Context ) string {
877+ func (a * versionsValidator ) Description (ctx context.Context ) string {
878878 return a .MarkdownDescription (ctx )
879879}
880880
881881// MarkdownDescription implements validator.List.
882- func (a * activeVersionValidator ) MarkdownDescription (context.Context ) string {
883- return "Validate that exactly one template version has active set to true ."
882+ func (a * versionsValidator ) MarkdownDescription (context.Context ) string {
883+ return "Validate that template version names are unique and that at most one version is active ."
884884}
885885
886886// ValidateList implements validator.List.
887- func (a * activeVersionValidator ) ValidateList (ctx context.Context , req validator.ListRequest , resp * validator.ListResponse ) {
887+ func (a * versionsValidator ) ValidateList (ctx context.Context , req validator.ListRequest , resp * validator.ListResponse ) {
888888 if req .ConfigValue .IsNull () || req .ConfigValue .IsUnknown () {
889889 return
890890 }
@@ -908,13 +908,13 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
908908 uniqueNames [version .Name .ValueString ()] = struct {}{}
909909 }
910910
911- // Check if only one item in Version has active set to true
911+ // Ensure at most one version is active
912912 active := false
913913 for _ , version := range data {
914- // `active` is required , so if it's null or unknown, this is Terraform
914+ // `active` defaults to false , so if it's null or unknown, this is Terraform
915915 // requesting an early validation.
916916 if version .Active .IsNull () || version .Active .IsUnknown () {
917- return
917+ continue
918918 }
919919 if version .Active .ValueBool () {
920920 if active {
@@ -924,12 +924,9 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
924924 active = true
925925 }
926926 }
927- if ! active {
928- resp .Diagnostics .AddError ("Client Error" , "At least one template version must be active." )
929- }
930927}
931928
932- var _ validator.List = & activeVersionValidator {}
929+ var _ validator.List = & versionsValidator {}
933930
934931type versionsPlanModifier struct {}
935932
@@ -956,6 +953,12 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
956953 return
957954 }
958955
956+ hasActiveVersion , diag := hasOneActiveVersion (configVersions )
957+ if diag .HasError () {
958+ resp .Diagnostics .Append (diag ... )
959+ return
960+ }
961+
959962 for i := range planVersions {
960963 hash , err := computeDirectoryHash (planVersions [i ].Directory .ValueString ())
961964 if err != nil {
@@ -974,6 +977,13 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
974977 // If this is the first read, init the private state value
975978 if lvBytes == nil {
976979 lv = make (LastVersionsByHash )
980+ // If there's no prior private state, this might be resource creation,
981+ // in which case one version must be active.
982+ if ! hasActiveVersion {
983+ resp .Diagnostics .AddError ("Client Error" , "At least one template version must be active when creating a" +
984+ " `coderd_template` resource.\n (Subsequent resource updates can be made without an active template in the list)." )
985+ return
986+ }
977987 } else {
978988 err := json .Unmarshal (lvBytes , & lv )
979989 if err != nil {
@@ -982,9 +992,37 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
982992 }
983993 }
984994
985- planVersions .reconcileVersionIDs (lv , configVersions )
995+ diag = planVersions .reconcileVersionIDs (lv , configVersions , hasActiveVersion )
996+ if diag .HasError () {
997+ resp .Diagnostics .Append (diag ... )
998+ return
999+ }
1000+
1001+ resp .PlanValue , diag = types .ListValueFrom (ctx , req .PlanValue .ElementType (ctx ), planVersions )
1002+ if diag .HasError () {
1003+ resp .Diagnostics .Append (diag ... )
1004+ }
1005+ }
9861006
987- resp .PlanValue , resp .Diagnostics = types .ListValueFrom (ctx , req .PlanValue .ElementType (ctx ), planVersions )
1007+ func hasOneActiveVersion (data Versions ) (hasActiveVersion bool , diags diag.Diagnostics ) {
1008+ active := false
1009+ for _ , version := range data {
1010+ if version .Active .IsNull () || version .Active .IsUnknown () {
1011+ // If null or unknown, the value will be defaulted to false
1012+ continue
1013+ }
1014+ if version .Active .ValueBool () {
1015+ if active {
1016+ diags .AddError ("Client Error" , "Only one template version can be active at a time." )
1017+ return
1018+ }
1019+ active = true
1020+ }
1021+ }
1022+ if ! active {
1023+ return false , diags
1024+ }
1025+ return true , diags
9881026}
9891027
9901028func NewVersionsPlanModifier () planmodifier.List {
@@ -1309,6 +1347,7 @@ type PreviousTemplateVersion struct {
13091347 ID uuid.UUID `json:"id"`
13101348 Name string `json:"name"`
13111349 TFVars map [string ]string `json:"tf_vars"`
1350+ Active bool `json:"active"`
13121351}
13131352
13141353type privateState interface {
@@ -1331,13 +1370,15 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
13311370 ID : version .ID .ValueUUID (),
13321371 Name : version .Name .ValueString (),
13331372 TFVars : tfVars ,
1373+ Active : version .Active .ValueBool (),
13341374 })
13351375 } else {
13361376 lv [version .DirectoryHash .ValueString ()] = []PreviousTemplateVersion {
13371377 {
13381378 ID : version .ID .ValueUUID (),
13391379 Name : version .Name .ValueString (),
13401380 TFVars : tfVars ,
1381+ Active : version .Active .ValueBool (),
13411382 },
13421383 }
13431384 }
@@ -1350,7 +1391,7 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
13501391 return ps .SetKey (ctx , LastVersionsKey , lvBytes )
13511392}
13521393
1353- func (planVersions Versions ) reconcileVersionIDs (lv LastVersionsByHash , configVersions Versions ) {
1394+ func (planVersions Versions ) reconcileVersionIDs (lv LastVersionsByHash , configVersions Versions , hasOneActiveVersion bool ) ( diag diag. Diagnostics ) {
13541395 // We remove versions that we've matched from `lv`, so make a copy for
13551396 // resolving tfvar changes at the end.
13561397 fullLv := make (LastVersionsByHash )
@@ -1420,6 +1461,39 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe
14201461 }
14211462 }
14221463 }
1464+
1465+ // If a version was deactivated, and no active version was set, we need to
1466+ // return an error to avoid a post-apply plan being non-empty.
1467+ if ! hasOneActiveVersion {
1468+ for i := range planVersions {
1469+ if ! planVersions [i ].ID .IsUnknown () {
1470+ prevs , ok := fullLv [planVersions [i ].DirectoryHash .ValueString ()]
1471+ if ! ok {
1472+ continue
1473+ }
1474+ if versionDeactivated (prevs , & planVersions [i ]) {
1475+ diag .AddError ("Client Error" , "Plan could not determine which version should be active.\n " +
1476+ "Either specify an active version or modify the contents of the previously active version before marking it as inactive." )
1477+ return diag
1478+ }
1479+ }
1480+ }
1481+ }
1482+ return diag
1483+ }
1484+
1485+ func versionDeactivated (prevs []PreviousTemplateVersion , planned * TemplateVersion ) bool {
1486+ for _ , prev := range prevs {
1487+ if prev .ID == planned .ID .ValueUUID () {
1488+ if prev .Active &&
1489+ ! planned .Active .IsNull () &&
1490+ ! planned .Active .IsUnknown () &&
1491+ ! planned .Active .ValueBool () {
1492+ return true
1493+ }
1494+ }
1495+ }
1496+ return false
14231497}
14241498
14251499func tfVariablesChanged (prevs []PreviousTemplateVersion , planned * TemplateVersion ) bool {
0 commit comments