Skip to content

Commit 8ccc9dd

Browse files
Christine WANJAUCopilot
andcommitted
{appconfig} az appconfig enhanced-feature-flag set: Add --flag and top-level set arguments
Support defining the entire enhanced feature flag with --flag using full JSON or shorthand syntax (mirroring the data-plane API schema), and add top-level --enable/--description/--tags/--telemetry-enabled arguments. --flag is a full replacement and is mutually exclusive with the per-property arguments. Re-recorded the scenario test cassette. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 607d58f commit 8ccc9dd

7 files changed

Lines changed: 667 additions & 204 deletions

File tree

‎src/azure-cli/azure/cli/command_modules/appconfig/_featuremodels.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@
1919

2020
logger = get_logger(__name__)
2121

22+
23+
def parse_feature_flag_input(value):
24+
"""Parse the --flag value as AAZ shorthand syntax, falling back to JSON.
25+
26+
Mirrors AAZAnyTypeArgAction.decode_str: shorthand is tried first and, because the shorthand
27+
parser rejects double-quoted JSON, valid JSON falls through to shell_safe_json_parse. The
28+
@file/@- convention is expanded to file content by the CLI core before this runs.
29+
"""
30+
from azure.cli.core.aaz._utils import AAZShortHandSyntaxParser
31+
from azure.cli.core.aaz.exceptions import AAZInvalidShorthandSyntaxError
32+
33+
try:
34+
return AAZShortHandSyntaxParser()(value)
35+
except AAZInvalidShorthandSyntaxError:
36+
return shell_safe_json_parse(value, preserve_order=True)
37+
38+
2239
# Feature Flag Models #
2340

2441

‎src/azure-cli/azure/cli/command_modules/appconfig/_help.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,12 @@
627627
- name: Set an enhanced feature flag with null label using connection string and set a description.
628628
text:
629629
az appconfig enhanced-feature-flag set --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --description "This is a colorful feature"
630+
- name: Enable an enhanced feature flag while setting it.
631+
text:
632+
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --enable
633+
- name: Disable an enhanced feature flag while setting it.
634+
text:
635+
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --enable false
630636
- name: Set an enhanced feature flag using your 'az login' credentials.
631637
text:
632638
az appconfig enhanced-feature-flag set --endpoint https://myappconfiguration.azconfig.io --feature color --label MyLabel --auth-mode login
@@ -639,6 +645,18 @@
639645
- name: Disable telemetry on an enhanced feature flag.
640646
text:
641647
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --telemetry-enabled false
648+
- name: Set the entire enhanced feature flag from an inline JSON object.
649+
text:
650+
az appconfig enhanced-feature-flag set -n MyAppConfiguration --flag '{"name":"Beta","enabled":true,"conditions":{"requirement_type":"Any","filters":[{"name":"Microsoft.TimeWindow","parameters":{"Start":"Wed, 01 Jan 2025 00:00:00 GMT"}}]}}'
651+
- name: Set the entire enhanced feature flag using shorthand syntax.
652+
text:
653+
az appconfig enhanced-feature-flag set -n MyAppConfiguration --flag "{name:Beta,enabled:true,conditions:{requirement_type:Any,filters:[{name:Microsoft.TimeWindow,parameters:{Start:'Wed, 01 Jan 2025 00:00:00 GMT'}}]}}"
654+
- name: Set the entire enhanced feature flag from a JSON file.
655+
text:
656+
az appconfig enhanced-feature-flag set -n MyAppConfiguration --flag @flag.json
657+
- name: Set the entire enhanced feature flag by reading the JSON from stdin.
658+
text:
659+
cat flag.json | az appconfig enhanced-feature-flag set -n MyAppConfiguration --flag @-
642660
"""
643661

644662
helps['appconfig enhanced-feature-flag delete'] = """

‎src/azure-cli/azure/cli/command_modules/appconfig/_params.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
resource_group_name_type)
1515
from azure.cli.core.commands.validators import \
1616
get_default_location_from_resource_group
17+
from argcomplete.completers import FilesCompleter
1718
from ._constants import ImportExportProfiles, ImportMode, FeatureFlagConstants, ARMAuthenticationMode
19+
from ._featuremodels import parse_feature_flag_input
1820

1921
from ._validators import (validate_appservice_name_or_id, validate_aks_cluster_name_or_id,
2022
validate_sku, validate_snapshot_query_fields,
@@ -29,7 +31,7 @@
2931
validate_strict_import, validate_export_as_reference, validate_snapshot_filters,
3032
validate_snapshot_export, validate_snapshot_import, validate_tag_filters,
3133
validate_import_tag_filters, validate_dry_run, validate_kv_revision_retention_period,
32-
validate_public_network_args)
34+
validate_public_network_args, validate_feature_flag)
3335

3436

3537
def load_arguments(self, _):
@@ -472,15 +474,24 @@ def load_arguments(self, _):
472474

473475
with self.argument_context('appconfig enhanced-feature-flag') as c:
474476
c.argument('name', arg_type=data_plane_name_arg_type)
475-
c.argument('feature', validator=validate_feature, help="Name of the enhanced feature flag. Enhanced feature flag name cannot contain the '%' or ':' characters.")
477+
c.argument('feature', options_list=['--feature-name', '--feature'], validator=validate_feature, help="Name of the enhanced feature flag. Enhanced feature flag name cannot contain the '%' or ':' characters.")
476478
c.argument('label', help="If no label specified, uses the null label.")
477479

478480
with self.argument_context('appconfig enhanced-feature-flag set') as c:
481+
c.argument('enable', arg_type=get_three_state_flag(), help='Enable or disable the enhanced feature flag.')
479482
c.argument('description', help='Description of the enhanced feature flag to be set.')
480483
c.argument('requirement_type', arg_type=get_enum_type([FeatureFlagConstants.REQUIREMENT_TYPE_ALL, FeatureFlagConstants.REQUIREMENT_TYPE_ANY]),
481484
help='Requirement type determines if filters should use "Any" or "All" logic when evaluating the state of an enhanced feature flag.')
482485
c.argument('telemetry_enabled', arg_type=get_three_state_flag(), help='Enable or disable telemetry for the enhanced feature flag.')
483486
c.argument('tags', arg_type=tags_type)
487+
c.argument('flag', options_list=['--flag'], type=parse_feature_flag_input,
488+
completer=FilesCompleter(), validator=validate_feature_flag,
489+
help="The entire enhanced feature flag, using the same property names, casing and types as the "
490+
"data-plane API and 'az appconfig enhanced-feature-flag show' (name, enabled, description, "
491+
"conditions, allocation, variants, telemetry, tags). Accepts a JSON object, a shorthand-syntax "
492+
"object, a file path with @ prefix (e.g. @flag.json), or @- to read from stdin. Providing --flag "
493+
"replaces the entire feature flag and cannot be combined with --enable, --description, "
494+
"--requirement-type or --telemetry-enabled.")
484495

485496
with self.argument_context('appconfig enhanced-feature-flag show') as c:
486497
c.argument('fields', arg_type=enhanced_feature_fields_arg_type)

‎src/azure-cli/azure/cli/command_modules/appconfig/_validators.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
validate_feature_flag_key,
2525
is_http_endpoint)
2626
from ._models import QueryFields
27-
from ._constants import ImportExportProfiles
27+
from ._constants import ImportExportProfiles, FeatureFlagConstants
2828
from ._featuremodels import FeatureQueryFields
2929
from ._snapshotmodels import SnapshotQueryFields
3030

@@ -330,6 +330,72 @@ def validate_feature(namespace):
330330
validate_feature_flag_name(namespace.feature)
331331

332332

333+
# Properties allowed in the enhanced feature flag input accepted by --flag. Matches the schema
334+
# emitted by 'az appconfig enhanced-feature-flag show' and the data-plane /ff API, so a flag can be
335+
# shown, edited and set back.
336+
FEATURE_FLAG_PROPERTIES = {
337+
FeatureFlagConstants.NAME,
338+
FeatureFlagConstants.ENABLED,
339+
FeatureFlagConstants.LABEL,
340+
FeatureFlagConstants.DESCRIPTION,
341+
FeatureFlagConstants.CONDITIONS,
342+
FeatureFlagConstants.ALLOCATION,
343+
FeatureFlagConstants.VARIANTS,
344+
FeatureFlagConstants.TELEMETRY,
345+
FeatureFlagConstants.TAGS,
346+
}
347+
348+
# Read-only properties emitted by 'show' that are tolerated (and ignored) on input.
349+
FEATURE_FLAG_READONLY_PROPERTIES = {
350+
FeatureFlagConstants.LAST_MODIFIED,
351+
"etag",
352+
}
353+
354+
355+
def validate_feature_flag(namespace):
356+
if getattr(namespace, 'flag', None) is None:
357+
return
358+
359+
# --flag is a full-object replacement; it cannot be mixed with the per-property content flags.
360+
conflicting = {
361+
'--enable': namespace.enable,
362+
'--description': namespace.description,
363+
'--requirement-type': namespace.requirement_type,
364+
'--telemetry-enabled': namespace.telemetry_enabled,
365+
}
366+
specified = [option for option, value in conflicting.items() if value is not None]
367+
if specified:
368+
raise MutuallyExclusiveArgumentError(
369+
"--flag cannot be combined with {}. Provide the entire feature flag with --flag, "
370+
"or set individual properties without --flag.".format(", ".join(specified)))
371+
372+
flag = namespace.flag
373+
if not isinstance(flag, dict):
374+
raise InvalidArgumentValueError(
375+
"--flag must be a JSON object or shorthand-syntax object representing a single feature flag.")
376+
377+
allowed = FEATURE_FLAG_PROPERTIES | FEATURE_FLAG_READONLY_PROPERTIES
378+
unknown_keys = set(flag.keys()) - allowed
379+
if unknown_keys:
380+
raise InvalidArgumentValueError(
381+
"Unsupported feature flag propert{}: {}. Allowed properties: {}.".format(
382+
"y" if len(unknown_keys) == 1 else "ies",
383+
", ".join(sorted(unknown_keys)),
384+
", ".join(sorted(FEATURE_FLAG_PROPERTIES))))
385+
386+
flag_name = flag.get(FeatureFlagConstants.NAME)
387+
if namespace.feature is None and flag_name is None:
388+
raise RequiredArgumentMissingError(
389+
"The feature flag 'name' is required. Provide it inside --flag or via --feature-name.")
390+
391+
if namespace.feature is not None and flag_name is not None and namespace.feature != flag_name:
392+
raise MutuallyExclusiveArgumentError(
393+
"Feature name mismatch: --feature-name '{}' does not match the 'name' '{}' in --flag. "
394+
"Provide only one, or make them identical.".format(namespace.feature, flag_name))
395+
396+
validate_feature_flag_name(flag_name if flag_name is not None else namespace.feature)
397+
398+
333399
def validate_feature_key(namespace):
334400
if namespace.key is not None:
335401
validate_feature_flag_key(namespace.key)

‎src/azure-cli/azure/cli/command_modules/appconfig/enhanced_feature.py‎

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,62 @@
2424

2525

2626
def set_feature(cmd,
27-
feature,
27+
feature=None,
2828
name=None,
2929
label=None,
30+
enable=None,
3031
description=None,
3132
requirement_type=None,
3233
telemetry_enabled=None,
3334
tags=None,
35+
flag=None,
3436
yes=False,
3537
connection_string=None,
3638
auth_mode="key",
3739
endpoint=None):
3840
feature_flag_client = get_appconfig_feature_flag_client(cmd, name, connection_string, auth_mode, endpoint)
3941

40-
try:
41-
feature_flag = feature_flag_client.get_feature_flag(name=feature, label=label)
42-
except ResourceNotFoundError:
43-
feature_flag = None
44-
except HttpResponseError as exception:
45-
raise CLIErrors.AzureResponseError("Failed to retrieve enhanced feature flag from config store. " + str(exception))
46-
47-
if feature_flag is None:
48-
feature_flag = FeatureFlag(name=feature, enabled=False, label=label)
49-
50-
if description is not None:
51-
feature_flag.description = description
52-
53-
if requirement_type is not None:
54-
if feature_flag.conditions is None:
55-
feature_flag.conditions = FeatureFlagConditions()
56-
feature_flag.conditions.requirement_type = requirement_type
57-
58-
if telemetry_enabled is not None:
59-
if feature_flag.telemetry is None:
60-
feature_flag.telemetry = FeatureFlagTelemetryConfiguration(enabled=telemetry_enabled)
61-
else:
62-
feature_flag.telemetry.enabled = telemetry_enabled
63-
64-
if tags is not None:
65-
feature_flag.tags = tags
42+
if flag is not None:
43+
# --flag mirrors the data-plane /ff schema (also emitted by 'show'), so deserialize it with the SDK.
44+
flag_to_load = {k: v for k, v in flag.items()
45+
if k not in (FeatureFlagConstants.LAST_MODIFIED, "etag")}
46+
if feature is not None:
47+
flag_to_load[FeatureFlagConstants.NAME] = feature
48+
feature_flag = FeatureFlag.from_dict(flag_to_load)
49+
if label is not None:
50+
feature_flag.label = label
51+
if tags is not None:
52+
feature_flag.tags = tags
53+
else:
54+
try:
55+
feature_flag = feature_flag_client.get_feature_flag(name=feature, label=label)
56+
except ResourceNotFoundError:
57+
feature_flag = None
58+
except HttpResponseError as exception:
59+
raise CLIErrors.AzureResponseError("Failed to retrieve enhanced feature flag from config store. " + str(exception))
60+
61+
if feature_flag is None:
62+
feature_flag = FeatureFlag(name=feature, enabled=False, label=label)
63+
64+
if enable is not None:
65+
feature_flag.enabled = enable
66+
67+
if description is not None:
68+
feature_flag.description = description
69+
70+
if requirement_type is not None:
71+
if feature_flag.conditions is None:
72+
feature_flag.conditions = FeatureFlagConditions()
73+
feature_flag.conditions.requirement_type = requirement_type
74+
75+
if telemetry_enabled is not None:
76+
if feature_flag.telemetry is None:
77+
feature_flag.telemetry = FeatureFlagTelemetryConfiguration(enabled=telemetry_enabled)
78+
else:
79+
feature_flag.telemetry.enabled = telemetry_enabled
80+
81+
if tags is not None:
82+
feature_flag.tags = tags
6683

6784
entry = json.dumps(_serialize_feature_flag(feature_flag), indent=2, sort_keys=True, ensure_ascii=False, default=str)
6885
confirmation_message = "Are you sure you want to set the enhanced feature flag: \n" + entry + "\n"

0 commit comments

Comments
 (0)