@@ -69,6 +69,7 @@ type TemplateResourceModel struct {
6969TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"`
7070RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
7171DeprecationMessage types.String `tfsdk:"deprecation_message"`
72+ MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
7273
7374// If null, we are not managing ACL via Terraform (such as for AGPL).
7475ACL types.Object `tfsdk:"acl"`
@@ -92,7 +93,9 @@ func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceMod
9293m .FailureTTLMillis .Equal (other .FailureTTLMillis )&&
9394m .TimeTilDormantMillis .Equal (other .TimeTilDormantMillis )&&
9495m .TimeTilDormantAutoDeleteMillis .Equal (other .TimeTilDormantAutoDeleteMillis )&&
95- m .RequireActiveVersion .Equal (other .RequireActiveVersion )
96+ m .RequireActiveVersion .Equal (other .RequireActiveVersion )&&
97+ m .DeprecationMessage .Equal (other .DeprecationMessage )&&
98+ m .MaxPortShareLevel .Equal (other .MaxPortShareLevel )
9699}
97100
98101func (m * TemplateResourceModel )CheckEntitlements (ctx context.Context ,features map [codersdk.FeatureName ]codersdk.Feature ) (diags diag.Diagnostics ) {
@@ -110,7 +113,8 @@ func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features
110113len (m .AutostartPermittedDaysOfWeek .Elements ())!= 7
111114requiresActiveVersion := m .RequireActiveVersion .ValueBool ()
112115requiresACL := ! m .ACL .IsNull ()
113- if requiresScheduling || requiresActiveVersion || requiresACL {
116+ requiresSharedPortsControl := m .MaxPortShareLevel .ValueString ()!= "" && m .MaxPortShareLevel .ValueString ()!= string (codersdk .WorkspaceAgentPortShareLevelPublic )
117+ if requiresScheduling || requiresActiveVersion || requiresACL || requiresSharedPortsControl {
114118if requiresScheduling && ! features [codersdk .FeatureAdvancedTemplateScheduling ].Enabled {
115119diags .AddError (
116120"Feature not enabled" ,
@@ -132,6 +136,13 @@ func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features
132136)
133137return
134138}
139+ if requiresSharedPortsControl && ! features [codersdk .FeatureControlSharedPorts ].Enabled {
140+ diags .AddError (
141+ "Feature not enabled" ,
142+ "Your license is not entitled to use port sharing control, so you cannot set max_port_share_level." ,
143+ )
144+ return
145+ }
135146}
136147return
137148}
@@ -369,6 +380,14 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
369380Computed :true ,
370381Default :booldefault .StaticBool (false ),
371382},
383+ "max_port_share_level" : schema.StringAttribute {
384+ MarkdownDescription :"(Enterprise) The maximum port share level for workspaces created from this template. Defaults to `owner` on an Enterprise deployment, or `public` otherwise." ,
385+ Optional :true ,
386+ Computed :true ,
387+ Validators : []validator.String {
388+ stringvalidator .OneOfCaseInsensitive (string (codersdk .WorkspaceAgentPortShareLevelAuthenticated ),string (codersdk .WorkspaceAgentPortShareLevelOwner ),string (codersdk .WorkspaceAgentPortShareLevelPublic )),
389+ },
390+ },
372391"deprecation_message" : schema.StringAttribute {
373392MarkdownDescription :"If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. Does nothing if set when the resource is created." ,
374393Optional :true ,
@@ -553,6 +572,24 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
553572data .ID = UUIDValue (templateResp .ID )
554573data .DisplayName = types .StringValue (templateResp .DisplayName )
555574
575+ // TODO: Remove this update call once this provider requires a Coder
576+ // deployment running `v2.15.0` or later.
577+ if data .MaxPortShareLevel .IsUnknown () {
578+ data .MaxPortShareLevel = types .StringValue (string (templateResp .MaxPortShareLevel ))
579+ }else {
580+
581+ mpslReq := data .toUpdateRequest (ctx ,& resp .Diagnostics )
582+ if resp .Diagnostics .HasError () {
583+ return
584+ }
585+ mpslResp ,err := client .UpdateTemplateMeta (ctx ,data .ID .ValueUUID (),* mpslReq )
586+ if err != nil {
587+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to set max port share level via update: %s" ,err ))
588+ return
589+ }
590+ data .MaxPortShareLevel = types .StringValue (string (mpslResp .MaxPortShareLevel ))
591+ }
592+
556593resp .Diagnostics .Append (data .Versions .setPrivateState (ctx ,resp .Private )... )
557594if resp .Diagnostics .HasError () {
558595return
@@ -591,6 +628,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
591628resp .Diagnostics .Append (diag ... )
592629return
593630}
631+ data .MaxPortShareLevel = types .StringValue (string (template .MaxPortShareLevel ))
594632
595633if ! data .ACL .IsNull () {
596634tflog .Info (ctx ,"reading template ACL" )
@@ -665,11 +703,16 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
665703
666704client := r .data .Client
667705
706+ // TODO(ethanndickson): Remove this once the provider requires a Coder
707+ // deployment running `v2.15.0` or later.
708+ if newState .MaxPortShareLevel .IsUnknown () {
709+ newState .MaxPortShareLevel = curState .MaxPortShareLevel
710+ }
668711templateMetadataChanged := ! newState .EqualTemplateMetadata (& curState )
669712// This is required, as the API will reject no-diff updates.
670713if templateMetadataChanged {
671714tflog .Info (ctx ,"change in template metadata detected, updating." )
672- updateReq := newState .toUpdateRequest (ctx ,resp )
715+ updateReq := newState .toUpdateRequest (ctx ,& resp . Diagnostics )
673716if resp .Diagnostics .HasError () {
674717return
675718}
@@ -758,6 +801,14 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
758801}
759802}
760803}
804+ // TODO(ethanndickson): Remove this once the provider requires a Coder
805+ // deployment running `v2.15.0` or later.
806+ templateResp ,err := client .Template (ctx ,templateID )
807+ if err != nil {
808+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template: %s" ,err ))
809+ return
810+ }
811+ newState .MaxPortShareLevel = types .StringValue (string (templateResp .MaxPortShareLevel ))
761812
762813resp .Diagnostics .Append (newState .Versions .setPrivateState (ctx ,resp .Private )... )
763814if resp .Diagnostics .HasError () {
@@ -1147,25 +1198,27 @@ func (r *TemplateResourceModel) readResponse(ctx context.Context, template *code
11471198r .TimeTilDormantAutoDeleteMillis = types .Int64Value (template .TimeTilDormantAutoDeleteMillis )
11481199r .RequireActiveVersion = types .BoolValue (template .RequireActiveVersion )
11491200r .DeprecationMessage = types .StringValue (template .DeprecationMessage )
1201+ // TODO(ethanndickson): MaxPortShareLevel deliberately omitted, as it can't
1202+ // be set during a create request, and we call this during `Create`.
11501203return nil
11511204}
11521205
1153- func (r * TemplateResourceModel )toUpdateRequest (ctx context.Context ,resp * resource. UpdateResponse )* codersdk.UpdateTemplateMeta {
1206+ func (r * TemplateResourceModel )toUpdateRequest (ctx context.Context ,diag * diag. Diagnostics )* codersdk.UpdateTemplateMeta {
11541207var days []string
1155- resp . Diagnostics .Append (
1208+ diag .Append (
11561209r .AutostartPermittedDaysOfWeek .ElementsAs (ctx ,& days ,false )... ,
11571210)
1158- if resp . Diagnostics .HasError () {
1211+ if diag .HasError () {
11591212return nil
11601213}
11611214autoStart := & codersdk.TemplateAutostartRequirement {
11621215DaysOfWeek :days ,
11631216}
11641217var reqs AutostopRequirement
1165- resp . Diagnostics .Append (
1218+ diag .Append (
11661219r .AutostopRequirement .As (ctx ,& reqs , basetypes.ObjectAsOptions {})... ,
11671220)
1168- if resp . Diagnostics .HasError () {
1221+ if diag .HasError () {
11691222return nil
11701223}
11711224autoStop := & codersdk.TemplateAutostopRequirement {
@@ -1189,6 +1242,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, resp *resou
11891242TimeTilDormantAutoDeleteMillis :r .TimeTilDormantAutoDeleteMillis .ValueInt64 (),
11901243RequireActiveVersion :r .RequireActiveVersion .ValueBool (),
11911244DeprecationMessage :r .DeprecationMessage .ValueStringPointer (),
1245+ MaxPortShareLevel :PtrTo (codersdk .WorkspaceAgentPortShareLevel (r .MaxPortShareLevel .ValueString ())),
11921246// If we're managing ACL, we want to delete the everyone group
11931247DisableEveryoneGroupAccess :! r .ACL .IsNull (),
11941248}