@@ -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 thatspecifes an icon to be displayed in the dashboard." ,
289+ MarkdownDescription :"Relative path or external URL thatspecifies an icon to be displayed in the dashboard." ,
290290Optional :true ,
291291Computed :true ,
292292Default :stringdefault .StaticString ("" ),
@@ -404,7 +404,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
404404Required :true ,
405405Validators : []validator.List {
406406listvalidator .SizeAtLeast (1 ),
407- NewActiveVersionValidator (),
407+ NewVersionsValidator (),
408408},
409409NestedObject : schema.NestedAttributeObject {
410410Attributes :map [string ]schema.Attribute {
@@ -867,24 +867,24 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
867867return []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 {
878878return a .MarkdownDescription (ctx )
879879}
880880
881881// MarkdownDescription implements validator.List.
882- func (a * activeVersionValidator )MarkdownDescription (context.Context )string {
883- return "Validate thatexactly one template versionhas active set to true ."
882+ func (a * versionsValidator )MarkdownDescription (context.Context )string {
883+ return "Validate that template versionnames 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 ) {
888888if req .ConfigValue .IsNull ()|| req .ConfigValue .IsUnknown () {
889889return
890890}
@@ -908,13 +908,13 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
908908uniqueNames [version .Name .ValueString ()]= struct {}{}
909909}
910910
911- //Check if only oneitem in Version has active set to true
911+ //Ensure at most oneversion is active
912912active := false
913913for _ ,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.
916916if version .Active .IsNull ()|| version .Active .IsUnknown () {
917- return
917+ continue
918918}
919919if version .Active .ValueBool () {
920920if active {
@@ -924,12 +924,9 @@ func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator
924924active = 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
956953return
957954}
958955
956+ hasActiveVersion ,diag := hasOneActiveVersion (configVersions )
957+ if diag .HasError () {
958+ resp .Diagnostics .Append (diag ... )
959+ return
960+ }
961+
959962for i := range planVersions {
960963hash ,err := computeDirectoryHash (planVersions [i ].Directory .ValueString ())
961964if 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
975978if lvBytes == nil {
976979lv = 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 {
978988err := json .Unmarshal (lvBytes ,& lv )
979989if 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 {
13091347ID uuid.UUID `json:"id"`
13101348Name string `json:"name"`
13111349TFVars 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
13311370ID :version .ID .ValueUUID (),
13321371Name :version .Name .ValueString (),
13331372TFVars :tfVars ,
1373+ Active :version .Active .ValueBool (),
13341374})
13351375}else {
13361376lv [version .DirectoryHash .ValueString ()]= []PreviousTemplateVersion {
13371377{
13381378ID :version .ID .ValueUUID (),
13391379Name :version .Name .ValueString (),
13401380TFVars :tfVars ,
1381+ Active :version .Active .ValueBool (),
13411382},
13421383}
13431384}
@@ -1350,7 +1391,7 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
13501391return 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.
13561397fullLv := 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 {