Skip to content

Commit a4f9739

Browse files
authored
[containerapp] az containerapp [create/update] --environment-mode WorkloadProfiles Add environment mode to az containerapp (#9551)
1 parent 48ed45b commit a4f9739

23 files changed

+41663
-53859
lines changed

src/containerapp/HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Release History
44
===============
55
upcoming
66
++++++
7+
* 'az containerapp env --environment-mode': Add environment mode to create and update commands
78

89
1.3.0b3
910
++++++

src/containerapp/azext_containerapp/_help.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,14 +331,14 @@
331331
--logs-workspace-id myLogsWorkspaceID \\
332332
--logs-workspace-key myLogsWorkspaceKey \\
333333
--location eastus2
334-
- name: Create an environment with workload profiles enabled.
334+
- name: Create an environment with workload profiles enabled (default mode).
335335
text: |
336336
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\
337-
--location eastus2 --enable-workload-profiles
338-
- name: Create an environment without workload profiles enabled.
337+
--location eastus2 --environment-mode WorkloadProfiles
338+
- name: Create an environment in consumption-only mode (no workload profiles).
339339
text: |
340340
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\
341-
--location eastus2 --enable-workload-profiles false
341+
--location eastus2 --environment-mode ConsumptionOnly
342342
- name: Create an environment with system assigned and user assigned identity.
343343
text: |
344344
az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\

src/containerapp/azext_containerapp/_params.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def load_arguments(self, _):
174174
with self.argument_context('containerapp env') as c:
175175
c.argument('public_network_access', arg_type=get_enum_type(['Enabled', 'Disabled']),
176176
help="Allow or block all public traffic", is_preview=True)
177+
c.argument('environment_mode', options_list=["--environment-mode"], help="Mode of the environment.", is_preview=True)
177178

178179
with self.argument_context('containerapp env', arg_group='Custom Domain') as c:
179180
c.argument('certificate_identity', options_list=['--custom-domain-certificate-identity', '--certificate-identity'],

src/containerapp/azext_containerapp/_up_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def __init__(
183183
workload_profile_type=None,
184184
workload_profile_name=None,
185185
is_env_for_azml_app=None,
186+
environment_mode=None
186187
):
187188
self.resource_type = None
188189
super().__init__(cmd, name, resource_group, exists)
@@ -211,6 +212,7 @@ def __init__(
211212
self.workload_profile_type = workload_profile_type
212213
self.workload_profile_name = workload_profile_name
213214
self.is_env_for_azml_app = is_env_for_azml_app
215+
self.environment_mode = environment_mode
214216

215217
def set_name(self, name_or_rid):
216218
if is_valid_resource_id(name_or_rid):
@@ -283,6 +285,7 @@ def create(self): # pylint: disable=arguments-differ
283285
workload_profile_type=self.workload_profile_type,
284286
workload_profile_name=self.workload_profile_name,
285287
is_env_for_azml_app=self.is_env_for_azml_app,
288+
environment_mode=self.environment_mode
286289
)
287290
self.exists = True
288291

src/containerapp/azext_containerapp/_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,19 @@ def get_cluster_extension(cmd, cluster_extension_id=None):
513513
extension_name=resource_name)
514514

515515

516+
def validate_environment_mode_and_workload_profiles_compatible(environment_mode, workload_profiles_enabled):
517+
# If only environment_mode is specified, derive enable_workload_profiles from it
518+
if environment_mode is not None:
519+
is_environment_mode_workload_profiles_enabled = environment_mode.lower() != 'consumptiononly'
520+
521+
# Check for conflicts when both are specified
522+
if workload_profiles_enabled is not None:
523+
if not is_environment_mode_workload_profiles_enabled and workload_profiles_enabled:
524+
raise ValidationError("Cannot use '--enable-workload-profiles' with '--environment-mode ConsumptionOnly'. Please use '--environment-mode' alone.")
525+
if is_environment_mode_workload_profiles_enabled and not workload_profiles_enabled:
526+
raise ValidationError("Cannot use '--enable-workload-profiles false' with '--environment-mode {}'. Please use '--environment-mode' alone.".format(environment_mode))
527+
528+
516529
def validate_custom_location(cmd, custom_location=None):
517530
if not is_valid_resource_id(custom_location):
518531
raise ValidationError('{} is not a valid Azure resource ID.'.format(custom_location))

src/containerapp/azext_containerapp/containerapp_env_decorator.py

Lines changed: 91 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from azure.cli.core.commands.client_factory import get_subscription_id
1414

1515
from ._models import ManagedServiceIdentity, CustomDomainConfiguration
16-
from ._utils import safe_get
16+
from ._utils import safe_get, validate_environment_mode_and_workload_profiles_compatible
1717
from ._client_factory import handle_non_404_status_code_exception
1818

1919
logger = get_logger(__name__)
@@ -31,7 +31,7 @@ def construct_payload(self):
3131
self.managed_env_def["tags"] = self.get_argument_tags()
3232
self.managed_env_def["properties"]["zoneRedundant"] = self.get_argument_zone_redundant()
3333

34-
self.set_up_workload_profiles()
34+
self._set_up_workload_profiles_and_environment_mode()
3535

3636
if self.get_argument_instrumentation_key() is not None:
3737
self.managed_env_def["properties"]["daprAIInstrumentationKey"] = self.get_argument_instrumentation_key()
@@ -43,24 +43,37 @@ def construct_payload(self):
4343
# copy end
4444

4545
# overwrite custom_domain_configuration
46-
self.set_up_custom_domain_configuration()
46+
self._set_up_custom_domain_configuration()
4747

48-
self.set_up_infrastructure_resource_group()
49-
self.set_up_dynamic_json_columns()
50-
self.set_up_managed_identity()
51-
self.set_up_public_network_access()
48+
self._set_up_infrastructure_resource_group()
49+
self._set_up_dynamic_json_columns()
50+
self._set_up_managed_identity()
51+
self._set_up_public_network_access()
5252

5353
def validate_arguments(self):
5454
super().validate_arguments()
5555

56+
# Check if user explicitly provided --enable-workload-profiles
57+
safe_params = self.cmd.cli_ctx.data.get('safe_params', [])
58+
user_provided_workload_profiles = '-w' in safe_params or '--enable-workload-profiles' in safe_params
59+
60+
# Only pass enable_workload_profiles if user explicitly provided it
61+
workload_profiles_value = self.get_argument_enable_workload_profiles() if user_provided_workload_profiles else None
62+
63+
# Resolve environment_mode and enable_workload_profiles
64+
validate_environment_mode_and_workload_profiles_compatible(
65+
self.get_argument_environment_mode(),
66+
workload_profiles_value
67+
)
68+
5669
# Infrastructure Resource Group
5770
if self.get_argument_infrastructure_resource_group() is not None:
5871
if not self.get_argument_infrastructure_subnet_resource_id():
5972
raise RequiredArgumentMissingError("Cannot use --infrastructure-resource-group/-i without "
6073
"--infrastructure-subnet-resource-id/-s")
61-
if not self.get_argument_enable_workload_profiles():
62-
raise RequiredArgumentMissingError("Cannot use --infrastructure-resource-group/-i without "
63-
"--enable-workload-profiles/-w")
74+
if not self._get_effective_workload_profiles():
75+
raise RequiredArgumentMissingError("Cannot use --infrastructure-resource-group/-i with "
76+
"--environment-mode ConsumptionOnly")
6477

6578
# validate custom domain configuration
6679
if self.get_argument_hostname():
@@ -69,20 +82,21 @@ def validate_arguments(self):
6982
if (not self.get_argument_certificate_file()) and (not self.get_argument_certificate_key_vault_url()):
7083
raise ValidationError("Either --certificate-file or --certificate-akv-url should be set when --dns-suffix is set")
7184

72-
def set_up_public_network_access(self):
85+
def _set_up_public_network_access(self):
7386
if self.get_argument_public_network_access():
7487
safe_set(self.managed_env_def, "properties", "publicNetworkAccess",
7588
value=self.get_argument_public_network_access())
7689

77-
def set_up_dynamic_json_columns(self):
90+
def _set_up_dynamic_json_columns(self):
7891
if self.get_argument_logs_destination() == "log-analytics" and self.get_argument_logs_dynamic_json_columns() is not None:
7992
safe_set(self.managed_env_def, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "dynamicJsonColumns", value=self.get_argument_logs_dynamic_json_columns())
8093

81-
def set_up_infrastructure_resource_group(self):
82-
if self.get_argument_enable_workload_profiles() and self.get_argument_infrastructure_subnet_resource_id() is not None:
94+
def _set_up_infrastructure_resource_group(self):
95+
effective_workload_profiles = self._get_effective_workload_profiles()
96+
if effective_workload_profiles and self.get_argument_infrastructure_subnet_resource_id() is not None:
8397
self.managed_env_def["properties"]["infrastructureResourceGroup"] = self.get_argument_infrastructure_resource_group()
8498

85-
def set_up_managed_identity(self):
99+
def _set_up_managed_identity(self):
86100
if self.get_argument_system_assigned() or self.get_argument_user_assigned():
87101
identity_def = ManagedServiceIdentity
88102
identity_def["type"] = "None"
@@ -109,19 +123,25 @@ def set_up_managed_identity(self):
109123
identity_def["userAssignedIdentities"][r] = {} # pylint: disable=unsupported-assignment-operation
110124
self.managed_env_def["identity"] = identity_def
111125

112-
def set_up_workload_profiles(self):
113-
if self.get_argument_enable_workload_profiles():
114-
# If the environment exists, infer the environment type
115-
existing_environment = None
116-
try:
117-
existing_environment = self.client.show(cmd=self.cmd, resource_group_name=self.get_argument_resource_group_name(), name=self.get_argument_name())
118-
except Exception as e:
119-
handle_non_404_status_code_exception(e)
120-
121-
if existing_environment and safe_get(existing_environment, "properties", "workloadProfiles") is None:
122-
# check if input params include -w/--enable-workload-profiles
123-
if self.cmd.cli_ctx.data.get('safe_params') and ('-w' in self.cmd.cli_ctx.data.get('safe_params') or '--enable-workload-profiles' in self.cmd.cli_ctx.data.get('safe_params')):
124-
raise ValidationError(f"Existing environment {self.get_argument_name()} cannot enable workload profiles. If you want to use Consumption and Dedicated environment, please create a new one.")
126+
# environment mode and workload profiles are coupled, so set them up together
127+
def _set_up_workload_profiles_and_environment_mode(self):
128+
# Use resolved effective value (supports both --environment-mode and --enable-workload-profiles)
129+
effective_workload_profiles = self._get_effective_workload_profiles()
130+
# If the environment exists, infer the environment type
131+
existing_environment = None
132+
environment_mode = self.get_argument_environment_mode()
133+
try:
134+
existing_environment = self.client.show(cmd=self.cmd, resource_group_name=self.get_argument_resource_group_name(), name=self.get_argument_name())
135+
except Exception as e:
136+
handle_non_404_status_code_exception(e)
137+
138+
if effective_workload_profiles:
139+
# Check if existing environment is ConsumptionOnly (no workload profiles)
140+
if existing_environment:
141+
if safe_get(existing_environment, "properties", "workloadProfiles") is None:
142+
if self.cmd.cli_ctx.data.get('safe_params') and ('-w' in self.cmd.cli_ctx.data.get('safe_params') or '--enable-workload-profiles' in self.cmd.cli_ctx.data.get('safe_params')):
143+
# User is trying to enable workload profiles on a ConsumptionOnly environment
144+
raise ValidationError(f"Existing environment {self.get_argument_name()} cannot enable workload profiles. If you want to use Consumption and Dedicated environment, please create a new one.")
125145
return
126146

127147
workload_profiles = get_default_workload_profiles(self.cmd, self.get_argument_location())
@@ -148,8 +168,18 @@ def set_up_workload_profiles(self):
148168
}
149169
workload_profiles.append(serverless_gpu_profile)
150170
self.managed_env_def["properties"]["workloadProfiles"] = workload_profiles
171+
else:
172+
# Check if existing environment is WorkloadProfiles
173+
if existing_environment:
174+
if safe_get(existing_environment, "properties", "workloadProfiles") is not None:
175+
# User is trying to enable workload profiles on a ConsumptionOnly environment
176+
raise ValidationError(f"Existing environment {self.get_argument_name()} cannot be a Consumption only environment. If you want to use Consumption only environment, please create a new one.")
177+
return
151178

152-
def set_up_custom_domain_configuration(self):
179+
if environment_mode:
180+
self.managed_env_def["properties"]["environmentMode"] = environment_mode
181+
182+
def _set_up_custom_domain_configuration(self):
153183
if self.get_argument_hostname():
154184
custom_domain = CustomDomainConfiguration
155185
custom_domain["dnsSuffix"] = self.get_argument_hostname()
@@ -175,6 +205,9 @@ def set_up_custom_domain_configuration(self):
175205
def get_argument_enable_workload_profiles(self):
176206
return self.get_param("enable_workload_profiles")
177207

208+
def get_argument_environment_mode(self):
209+
return self.get_param("environment_mode")
210+
178211
def get_argument_enable_dedicated_gpu(self):
179212
return self.get_param("enable_dedicated_gpu")
180213

@@ -205,6 +238,26 @@ def get_argument_workload_profile_type(self):
205238
def get_argument_workload_profile_name(self):
206239
return self.get_param("workload_profile_name")
207240

241+
def _get_effective_workload_profiles(self):
242+
243+
safe_params = self.cmd.cli_ctx.data.get('safe_params', [])
244+
245+
# First check if user provided --environment-mode
246+
if '--environment-mode' in safe_params:
247+
environment_mode = self.get_argument_environment_mode()
248+
if environment_mode:
249+
# WorkloadProfiles mode = workload profiles enabled
250+
# ConsumptionOnly = workload profiles disabled
251+
return environment_mode.lower() == "workloadprofiles"
252+
253+
# Fallback: check if user explicitly provided --enable-workload-profiles
254+
user_provided_wp = '-w' in safe_params or '--enable-workload-profiles' in safe_params
255+
if user_provided_wp:
256+
return self.get_argument_enable_workload_profiles()
257+
258+
# Default to True if neither --environment-mode nor --enable-workload-profiles was provided
259+
return True
260+
208261

209262
class ContainerappEnvPreviewUpdateDecorator(ContainerAppEnvUpdateDecorator):
210263
def validate_arguments(self):
@@ -218,6 +271,12 @@ def construct_payload(self):
218271
super().construct_payload()
219272

220273
self.set_up_public_network_access()
274+
self._set_up_environment_mode()
275+
276+
def _set_up_environment_mode(self):
277+
environment_mode = self.get_argument_environment_mode()
278+
if environment_mode:
279+
safe_set(self.managed_env_def, "properties", "environmentMode", value=environment_mode)
221280

222281
def set_up_public_network_access(self):
223282
if self.get_argument_public_network_access():
@@ -275,3 +334,6 @@ def get_argument_certificate_key_vault_url(self):
275334

276335
def get_argument_public_network_access(self):
277336
return self.get_param("public_network_access")
337+
338+
def get_argument_environment_mode(self):
339+
return self.get_param("environment_mode")

src/containerapp/azext_containerapp/custom.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,8 @@ def create_managed_environment(cmd,
764764
logs_dynamic_json_columns=False,
765765
system_assigned=False,
766766
user_assigned=None,
767-
public_network_access=None):
767+
public_network_access=None,
768+
environment_mode=None):
768769
return create_managed_environment_logic(
769770
cmd=cmd,
770771
name=name,
@@ -798,7 +799,8 @@ def create_managed_environment(cmd,
798799
logs_dynamic_json_columns=logs_dynamic_json_columns,
799800
system_assigned=system_assigned,
800801
user_assigned=user_assigned,
801-
public_network_access=public_network_access
802+
public_network_access=public_network_access,
803+
environment_mode=environment_mode
802804
)
803805

804806

@@ -837,7 +839,8 @@ def create_managed_environment_logic(cmd,
837839
public_network_access=None,
838840
workload_profile_type=None,
839841
workload_profile_name=None,
840-
is_env_for_azml_app=False):
842+
is_env_for_azml_app=False,
843+
environment_mode=None):
841844
raw_parameters = locals()
842845
containerapp_env_create_decorator = ContainerappEnvPreviewCreateDecorator(
843846
cmd=cmd,
@@ -876,7 +879,8 @@ def update_managed_environment(cmd,
876879
p2p_encryption_enabled=None,
877880
no_wait=False,
878881
logs_dynamic_json_columns=None,
879-
public_network_access=None):
882+
public_network_access=None,
883+
environment_mode=None):
880884
raw_parameters = locals()
881885
containerapp_env_update_decorator = ContainerappEnvPreviewUpdateDecorator(
882886
cmd=cmd,

0 commit comments

Comments
 (0)