@@ -12,6 +12,7 @@ import (
1212"github.com/google/uuid"
1313"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
1414"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
15+ "github.com/hashicorp/terraform-plugin-framework/attr"
1516"github.com/hashicorp/terraform-plugin-framework/path"
1617"github.com/hashicorp/terraform-plugin-framework/resource"
1718"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -22,6 +23,7 @@ import (
2223"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2324"github.com/hashicorp/terraform-plugin-framework/schema/validator"
2425"github.com/hashicorp/terraform-plugin-framework/types"
26+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
2527"github.com/hashicorp/terraform-plugin-log/tflog"
2628)
2729
@@ -51,20 +53,19 @@ type TemplateResourceModel struct {
5153AllowUserAutoStart types.Bool `tfsdk:"allow_user_auto_start"`
5254AllowUserAutoStop types.Bool `tfsdk:"allow_user_auto_stop"`
5355
54- ACL * ACL `tfsdk:"acl"`
55- Versions Versions `tfsdk:"versions"`
56+ ACL types. Object `tfsdk:"acl"`
57+ Versions Versions `tfsdk:"versions"`
5658}
5759
58- // EqualTemplateMetadata returns true if two templates have identical metadata& ACL.
60+ // EqualTemplateMetadata returns true if two templates have identical metadata(excluding ACL)
5961func (m TemplateResourceModel )EqualTemplateMetadata (other TemplateResourceModel )bool {
6062return m .Name .Equal (other .Name )&&
6163m .DisplayName .Equal (other .DisplayName )&&
6264m .Description .Equal (other .Description )&&
6365m .OrganizationID .Equal (other .OrganizationID )&&
6466m .Icon .Equal (other .Icon )&&
6567m .AllowUserAutoStart .Equal (other .AllowUserAutoStart )&&
66- m .AllowUserAutoStop .Equal (other .AllowUserAutoStop )&&
67- m .ACL .Equal (other .ACL )
68+ m .AllowUserAutoStop .Equal (other .AllowUserAutoStop )
6869}
6970
7071type TemplateVersion struct {
@@ -110,38 +111,10 @@ type ACL struct {
110111GroupPermissions []Permission `tfsdk:"groups"`
111112}
112113
113- func (a * ACL )Equal (other * ACL )bool {
114- if len (a .UserPermissions )!= len (other .UserPermissions ) {
115- return false
116- }
117- if len (a .GroupPermissions )!= len (other .GroupPermissions ) {
118- return false
119- }
120- for _ ,e1 := range a .UserPermissions {
121- found := false
122- for _ ,e2 := range other .UserPermissions {
123- if e1 .Equal (& e2 ) {
124- found = true
125- break
126- }
127- }
128- if ! found {
129- return false
130- }
131- }
132- for _ ,e1 := range a .GroupPermissions {
133- found := false
134- for _ ,e2 := range other .GroupPermissions {
135- if e1 .Equal (& e2 ) {
136- found = true
137- break
138- }
139- }
140- if ! found {
141- return false
142- }
143- }
144- return true
114+ // aclTypeAttr is the type schema for an instance of `ACL`.
115+ var aclTypeAttr = map [string ]attr.Type {
116+ "users" :permissionTypeAttr ,
117+ "groups" :permissionTypeAttr ,
145118}
146119
147120type Permission struct {
@@ -151,12 +124,8 @@ type Permission struct {
151124Role types.String `tfsdk:"role"`
152125}
153126
154- func (p * Permission )Equal (other * Permission )bool {
155- return p .ID .Equal (other .ID )&& p .Role .Equal (other .Role )
156- }
157-
158- // permissionsAttribute is the attribute schema for an instance of `[]Permission`.
159- var permissionsAttribute = schema.SetNestedAttribute {
127+ // permissionAttribute is the attribute schema for an instance of `[]Permission`.
128+ var permissionAttribute = schema.SetNestedAttribute {
160129Required :true ,
161130NestedObject : schema.NestedAttributeObject {
162131Attributes :map [string ]schema.Attribute {
@@ -165,14 +134,19 @@ var permissionsAttribute = schema.SetNestedAttribute{
165134},
166135"role" : schema.StringAttribute {
167136Required :true ,
168- Validators : []validator.String {
169- stringvalidator .OneOf ("admin" ,"use" ,"" ),
170- },
171137},
172138},
173139},
174140}
175141
142+ // permissionTypeAttr is the type schema for an instance of `[]Permission`.
143+ var permissionTypeAttr = basetypes.SetType {ElemType : types.ObjectType {
144+ AttrTypes :map [string ]attr.Type {
145+ "id" : basetypes.StringType {},
146+ "role" : basetypes.StringType {},
147+ },
148+ }}
149+
176150func (r * TemplateResource )Metadata (ctx context.Context ,req resource.MetadataRequest ,resp * resource.MetadataResponse ) {
177151resp .TypeName = req .ProviderTypeName + "_template"
178152}
@@ -234,11 +208,11 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
234208Default :booldefault .StaticBool (true ),
235209},
236210"acl" : schema.SingleNestedAttribute {
237- MarkdownDescription :"Access control list for the template." ,
238- Required :true ,
211+ MarkdownDescription :"Access control list for the template. Requires an enterprise Coder deployment. If null, ACL policies will not be added or removed by Terraform. " ,
212+ Optional :true ,
239213Attributes :map [string ]schema.Attribute {
240- "users" :permissionsAttribute ,
241- "groups" :permissionsAttribute ,
214+ "users" :permissionAttribute ,
215+ "groups" :permissionAttribute ,
242216},
243217},
244218"versions" : schema.ListNestedAttribute {
@@ -371,13 +345,22 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
371345"id" :templateResp .ID ,
372346})
373347
374- tflog .Trace (ctx ,"updating template ACL" )
375- err = client .UpdateTemplateACL (ctx ,templateResp .ID ,convertACLToRequest (data .ACL ))
376- if err != nil {
377- resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update template ACL: %s" ,err ))
378- return
348+ if ! data .ACL .IsNull () {
349+ tflog .Trace (ctx ,"updating template ACL" )
350+ var acl ACL
351+ resp .Diagnostics .Append (
352+ data .ACL .As (ctx ,& acl , basetypes.ObjectAsOptions {})... ,
353+ )
354+ if resp .Diagnostics .HasError () {
355+ return
356+ }
357+ err = client .UpdateTemplateACL (ctx ,templateResp .ID ,convertACLToRequest (acl ))
358+ if err != nil {
359+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to create template ACL: %s" ,err ))
360+ return
361+ }
362+ tflog .Trace (ctx ,"successfully updated template ACL" )
379363}
380- tflog .Trace (ctx ,"successfully updated template ACL" )
381364}
382365if version .Active .ValueBool () {
383366tflog .Trace (ctx ,"marking template version as active" ,map [string ]any {
@@ -430,12 +413,22 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
430413data .AllowUserAutoStart = types .BoolValue (template .AllowUserAutostart )
431414data .AllowUserAutoStop = types .BoolValue (template .AllowUserAutostop )
432415
433- acl ,err := client .TemplateACL (ctx ,templateID )
434- if err != nil {
435- resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template ACL: %s" ,err ))
436- return
416+ if ! data .ACL .IsNull () {
417+ tflog .Trace (ctx ,"reading template ACL" )
418+ acl ,err := client .TemplateACL (ctx ,templateID )
419+ if err != nil {
420+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template ACL: %s" ,err ))
421+ return
422+ }
423+ TFAcl := convertResponseToACL (acl )
424+ aclObj ,diag := types .ObjectValueFrom (ctx ,aclTypeAttr ,TFAcl )
425+ diag .Append (diag ... )
426+ if diag .HasError () {
427+ return
428+ }
429+ data .ACL = aclObj
430+ tflog .Trace (ctx ,"read template ACL" )
437431}
438- data .ACL = convertResponseToACL (acl )
439432
440433for idx ,version := range data .Versions {
441434versionID := version .ID .ValueUUID ()
@@ -500,11 +493,20 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
500493DisableEveryoneGroupAccess :true ,
501494})
502495if err != nil {
503- resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update template: %s" ,err ))
496+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update template metadata : %s" ,err ))
504497return
505498}
506499tflog .Trace (ctx ,"successfully updated template metadata" )
507- err = client .UpdateTemplateACL (ctx ,templateID ,convertACLToRequest (planState .ACL ))
500+ }
501+
502+ // If there's a change, and we're still managing ACL
503+ if ! planState .ACL .Equal (curState .ACL )&& ! planState .ACL .IsNull () {
504+ var acl ACL
505+ resp .Diagnostics .Append (planState .ACL .As (ctx ,& acl , basetypes.ObjectAsOptions {})... )
506+ if resp .Diagnostics .HasError () {
507+ return
508+ }
509+ err := client .UpdateTemplateACL (ctx ,templateID ,convertACLToRequest (acl ))
508510if err != nil {
509511resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to update template ACL: %s" ,err ))
510512return
@@ -784,10 +786,7 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
784786return & versionResp ,nil
785787}
786788
787- func convertACLToRequest (permissions * ACL ) codersdk.UpdateTemplateACL {
788- if permissions == nil {
789- return codersdk.UpdateTemplateACL {}
790- }
789+ func convertACLToRequest (permissions ACL ) codersdk.UpdateTemplateACL {
791790var userPerms = make (map [string ]codersdk.TemplateRole )
792791for _ ,perm := range permissions .UserPermissions {
793792userPerms [perm .ID .ValueString ()]= codersdk .TemplateRole (perm .Role .ValueString ())
@@ -802,7 +801,7 @@ func convertACLToRequest(permissions *ACL) codersdk.UpdateTemplateACL {
802801}
803802}
804803
805- func convertResponseToACL (acl codersdk.TemplateACL )* ACL {
804+ func convertResponseToACL (acl codersdk.TemplateACL )ACL {
806805userPerms := make ([]Permission ,0 ,len (acl .Users ))
807806for _ ,user := range acl .Users {
808807userPerms = append (userPerms ,Permission {
@@ -817,7 +816,7 @@ func convertResponseToACL(acl codersdk.TemplateACL) *ACL {
817816Role :types .StringValue (string (group .Role )),
818817})
819818}
820- return & ACL {
819+ return ACL {
821820UserPermissions :userPerms ,
822821GroupPermissions :groupPerms ,
823822}