@@ -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