@@ -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,23 @@ 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+ mpslReq := data .toUpdateRequest (ctx ,& resp .Diagnostics )
581+ if resp .Diagnostics .HasError () {
582+ return
583+ }
584+ mpslResp ,err := client .UpdateTemplateMeta (ctx ,data .ID .ValueUUID (),* mpslReq )
585+ if err != nil {
586+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to set max port share level via update: %s" ,err ))
587+ return
588+ }
589+ data .MaxPortShareLevel = types .StringValue (string (mpslResp .MaxPortShareLevel ))
590+ }
591+
556592resp .Diagnostics .Append (data .Versions .setPrivateState (ctx ,resp .Private )... )
557593if resp .Diagnostics .HasError () {
558594return
@@ -591,6 +627,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
591627resp .Diagnostics .Append (diag ... )
592628return
593629}
630+ data .MaxPortShareLevel = types .StringValue (string (template .MaxPortShareLevel ))
594631
595632if ! data .ACL .IsNull () {
596633tflog .Info (ctx ,"reading template ACL" )
@@ -665,11 +702,16 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
665702
666703client := r .data .Client
667704
705+ // TODO(ethanndickson): Remove this once the provider requires a Coder
706+ // deployment running `v2.15.0` or later.
707+ if newState .MaxPortShareLevel .IsUnknown () {
708+ newState .MaxPortShareLevel = curState .MaxPortShareLevel
709+ }
668710templateMetadataChanged := ! newState .EqualTemplateMetadata (& curState )
669711// This is required, as the API will reject no-diff updates.
670712if templateMetadataChanged {
671713tflog .Info (ctx ,"change in template metadata detected, updating." )
672- updateReq := newState .toUpdateRequest (ctx ,resp )
714+ updateReq := newState .toUpdateRequest (ctx ,& resp . Diagnostics )
673715if resp .Diagnostics .HasError () {
674716return
675717}
@@ -758,6 +800,14 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
758800}
759801}
760802}
803+ // TODO(ethanndickson): Remove this once the provider requires a Coder
804+ // deployment running `v2.15.0` or later.
805+ templateResp ,err := client .Template (ctx ,templateID )
806+ if err != nil {
807+ resp .Diagnostics .AddError ("Client Error" ,fmt .Sprintf ("Failed to get template: %s" ,err ))
808+ return
809+ }
810+ newState .MaxPortShareLevel = types .StringValue (string (templateResp .MaxPortShareLevel ))
761811
762812resp .Diagnostics .Append (newState .Versions .setPrivateState (ctx ,resp .Private )... )
763813if resp .Diagnostics .HasError () {
@@ -1147,25 +1197,27 @@ func (r *TemplateResourceModel) readResponse(ctx context.Context, template *code
11471197r .TimeTilDormantAutoDeleteMillis = types .Int64Value (template .TimeTilDormantAutoDeleteMillis )
11481198r .RequireActiveVersion = types .BoolValue (template .RequireActiveVersion )
11491199r .DeprecationMessage = types .StringValue (template .DeprecationMessage )
1200+ // TODO(ethanndickson): MaxPortShareLevel deliberately omitted, as it can't
1201+ // be set during a create request, and we call this during `Create`.
11501202return nil
11511203}
11521204
1153- func (r * TemplateResourceModel )toUpdateRequest (ctx context.Context ,resp * resource. UpdateResponse )* codersdk.UpdateTemplateMeta {
1205+ func (r * TemplateResourceModel )toUpdateRequest (ctx context.Context ,diag * diag. Diagnostics )* codersdk.UpdateTemplateMeta {
11541206var days []string
1155- resp . Diagnostics .Append (
1207+ diag .Append (
11561208r .AutostartPermittedDaysOfWeek .ElementsAs (ctx ,& days ,false )... ,
11571209)
1158- if resp . Diagnostics .HasError () {
1210+ if diag .HasError () {
11591211return nil
11601212}
11611213autoStart := & codersdk.TemplateAutostartRequirement {
11621214DaysOfWeek :days ,
11631215}
11641216var reqs AutostopRequirement
1165- resp . Diagnostics .Append (
1217+ diag .Append (
11661218r .AutostopRequirement .As (ctx ,& reqs , basetypes.ObjectAsOptions {})... ,
11671219)
1168- if resp . Diagnostics .HasError () {
1220+ if diag .HasError () {
11691221return nil
11701222}
11711223autoStop := & codersdk.TemplateAutostopRequirement {
@@ -1189,6 +1241,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, resp *resou
11891241TimeTilDormantAutoDeleteMillis :r .TimeTilDormantAutoDeleteMillis .ValueInt64 (),
11901242RequireActiveVersion :r .RequireActiveVersion .ValueBool (),
11911243DeprecationMessage :r .DeprecationMessage .ValueStringPointer (),
1244+ MaxPortShareLevel :PtrTo (codersdk .WorkspaceAgentPortShareLevel (r .MaxPortShareLevel .ValueString ())),
11921245// If we're managing ACL, we want to delete the everyone group
11931246DisableEveryoneGroupAccess :! r .ACL .IsNull (),
11941247}