From 7f6f7af71b3c3aed11586c01465fa962d53ea5e5 Mon Sep 17 00:00:00 2001 From: Christine WANJAU Date: Mon, 24 Aug 2026 15:07:44 +0300 Subject: [PATCH] [App Configuration] Add description support for key-values and snapshots --- .../cli/command_modules/appconfig/_help.py | 11 +++++-- .../cli/command_modules/appconfig/_models.py | 18 +++++++--- .../cli/command_modules/appconfig/_params.py | 8 +++-- .../appconfig/_snapshotmodels.py | 13 ++++++-- .../cli/command_modules/appconfig/keyvalue.py | 33 ++++++++++++++----- .../cli/command_modules/appconfig/snapshot.py | 6 ++-- .../latest/test_appconfig_kv_commands.py | 31 +++++++++++++++++ .../test_appconfig_snapshot_commands.py | 16 ++++++--- 8 files changed, 109 insertions(+), 27 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/_help.py b/src/azure-cli/azure/cli/command_modules/appconfig/_help.py index 5aab1598661..e449e2738ba 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/_help.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/_help.py @@ -286,6 +286,8 @@ text: az appconfig kv set -n MyAppConfiguration --key foo --value null --content-type application/json - name: Set a key-value using your 'az login' credentials. text: az appconfig kv set --endpoint https://contoso.azconfig.io --key color --value red --auth-mode login + - name: Set a key-value with a description. + text: az appconfig kv set -n MyAppConfiguration --key color --value red --description "The theme color" """ helps['appconfig kv set-keyvault'] = """ @@ -296,6 +298,8 @@ text: az appconfig kv set-keyvault -n MyAppConfiguration --key HostSecret --label MyLabel --secret-identifier https://contoso.vault.azure.net/Secrets/DummySecret/Dummyversion - name: Set a keyvault reference with null label and multiple tags using connection string. text: az appconfig kv set-keyvault --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --key HostSecret --secret-identifier https://contoso.vault.azure.net/Secrets/DummySecret --tags tag1=value1 tag2=value2 + - name: Set a keyvault reference with a description. + text: az appconfig kv set-keyvault -n MyAppConfiguration --key HostSecret --secret-identifier https://contoso.vault.azure.net/Secrets/DummySecret --description "Reference to the host secret" """ helps['appconfig kv set-snapshot-reference'] = """ @@ -308,6 +312,8 @@ text: az appconfig kv set-snapshot-reference --endpoint https://contoso.azconfig.io --key MySnapshotRef --snapshot-name MySnapshot --auth-mode login - name: Set a snapshot reference with tags using connection string. text: az appconfig kv set-snapshot-reference --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --key MySnapshotRef --snapshot-name MySnapshot --tags tag1=value1 tag2=value2 + - name: Set a snapshot reference with a description. + text: az appconfig kv set-snapshot-reference -n MyAppConfiguration --key MySnapshotRef --snapshot-name MySnapshot --description "Reference to MySnapshot" """ helps['appconfig kv show'] = """ @@ -745,8 +751,9 @@ az appconfig snapshot create -s MySnapshot -n MyAppConfiguration --filters '{\\"key\\":\\"app/*\\"}' '{\\"key\\":\\"app/*\\", \\"label\\":\\"prod\\"}' --composition-type 'key' - name: Create a snapshot of all keys starting with 'Test' and have tags 'tag1=value1' and 'tag2=value2'. text: - az appconfig snapshot create -s MySnapshot -n MyAppConfiguration --filters '{\\"key\\":\\"Test*\\", \\"tags\\":[\\"tag1=value1\\", \\"tag2=value2\\"]}' - """ + az appconfig snapshot create -s MySnapshot -n MyAppConfiguration --filters '{\\"key\\":\\"Test*\\", \\"tags\\":[\\"tag1=value1\\", \\"tag2=value2\\"]}' - name: Create a snapshot MySnapshot with a description. + text: + az appconfig snapshot create -s MySnapshot -n MyAppConfiguration --filters '{\"key\":\"Test*\"}' --description "Snapshot of Test key-values" """ helps['appconfig snapshot show'] = """ type: command diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/_models.py b/src/azure-cli/azure/cli/command_modules/appconfig/_models.py index a4bf52ff945..2d32e70d3bc 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/_models.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/_models.py @@ -21,7 +21,8 @@ class QueryFields(Enum): LAST_MODIFIED = 0x020 LOCKED = 0x040 TAGS = 0x080 - ALL = KEY | LABEL | VALUE | CONTENT_TYPE | ETAG | LAST_MODIFIED | LOCKED | TAGS + DESCRIPTION = 0x100 + ALL = KEY | LABEL | VALUE | CONTENT_TYPE | ETAG | LAST_MODIFIED | LOCKED | TAGS | DESCRIPTION class KeyValue: @@ -45,6 +46,8 @@ class KeyValue: Represents whether the key value entry is locked. :ivar str last_modified: A str representation of the datetime object representing the last time the key was modified. + :ivar str description: + Description of the entry. ''' def __init__(self, @@ -55,7 +58,8 @@ def __init__(self, content_type=None, etag=None, locked=False, - last_modified=None): + last_modified=None, + description=None): self.key = key self.value = value self.label = label @@ -64,6 +68,7 @@ def __init__(self, self.etag = etag self.last_modified = last_modified.isoformat() if isinstance(last_modified, datetime) else str(last_modified) self.locked = locked + self.description = description def __str__(self): return "\nKey: " + self.key + \ @@ -73,7 +78,8 @@ def __str__(self): "\nLast Modified: " + self.last_modified + \ "\nLocked: " + self.locked + \ "\nContent Type: " + self.content_type + \ - "\nTags: " + (str(self.tags) if self.tags else '') + "\nTags: " + (str(self.tags) if self.tags else '') + \ + "\nDescription: " + (self.description if self.description else '') def convert_configurationsetting_to_keyvalue(configuration_setting=None): @@ -87,7 +93,8 @@ def convert_configurationsetting_to_keyvalue(configuration_setting=None): last_modified=configuration_setting.last_modified, tags=configuration_setting.tags, locked=configuration_setting.read_only, - etag=configuration_setting.etag) + etag=configuration_setting.etag, + description=getattr(configuration_setting, 'description', None)) def convert_keyvalue_to_configurationsetting(keyvalue=None): @@ -99,4 +106,5 @@ def convert_keyvalue_to_configurationsetting(keyvalue=None): value=keyvalue.value, tags=keyvalue.tags, read_only=keyvalue.locked, - etag=keyvalue.etag) + etag=keyvalue.etag, + description=keyvalue.description) diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/_params.py b/src/azure-cli/azure/cli/command_modules/appconfig/_params.py index 66ad0403882..f28e4e66069 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/_params.py @@ -39,7 +39,7 @@ def load_arguments(self, _): nargs='+', help='Space-separated customized output fields.', validator=validate_query_fields, - arg_type=get_enum_type(['key', 'value', 'label', 'content_type', 'etag', 'tags', 'locked', 'last_modified']) + arg_type=get_enum_type(['key', 'value', 'label', 'content_type', 'etag', 'tags', 'locked', 'last_modified', 'description']) ) feature_fields_arg_type = CLIArgumentType( nargs='+', @@ -51,7 +51,7 @@ def load_arguments(self, _): nargs='+', help='Customize output fields for Snapshots', validator=validate_snapshot_query_fields, - arg_type=get_enum_type(['name', 'etag', 'retention_period', 'filters', 'status', 'created', 'expires', 'size', 'items_count', 'composition_type', 'tags']) + arg_type=get_enum_type(['name', 'etag', 'retention_period', 'filters', 'status', 'created', 'expires', 'size', 'items_count', 'composition_type', 'tags', 'description']) ) filter_parameters_arg_type = CLIArgumentType( validator=validate_filter_parameters, @@ -330,17 +330,20 @@ def load_arguments(self, _): c.argument('tags', arg_type=tags_type) c.argument('content_type', help='Content type of the key-value to be set.') c.argument('value', help='Value of the key-value to be set.') + c.argument('description', help='Description of the key-value to be set.') with self.argument_context('appconfig kv set-keyvault') as c: c.argument('key', validator=validate_key, help="Key to be set. Key cannot be a '.' or '..', or contain the '%' character.") c.argument('label', help="If no label specified, set the key with null label by default") c.argument('tags', arg_type=tags_type) + c.argument('description', help='Description of the key vault reference to be set.') c.argument('secret_identifier', validator=validate_secret_identifier, help="ID of the Key Vault object. Can be found using 'az keyvault {collection} show' command, where collection is key, secret or certificate. To set reference to the latest version of your secret, remove version information from secret identifier.") with self.argument_context('appconfig kv set-snapshot-reference') as c: c.argument('key', validator=validate_key, help="Key to be set. Key cannot be a '.' or '..', or contain the '%' character.") c.argument('label', help="If no label specified, set the key with null label by default") c.argument('tags', arg_type=tags_type) + c.argument('description', help='Description of the snapshot reference to be set.') c.argument('snapshot_name', validator=validate_snapshot_reference, help='Name of the snapshot to reference. This is required.') with self.argument_context('appconfig kv delete') as c: @@ -476,6 +479,7 @@ def load_arguments(self, _): c.argument('composition_type', arg_type=get_enum_type(["key", "key_label"]), help='Composition type used in building App Configuration snapshots. If not specified, defaults to key.') c.argument('retention_period', type=int, help='Duration in seconds for which a snapshot can remain archived before expiry. A snapshot can be archived for a maximum of 7 days (604,800s) for free and developer tier stores and 90 days (7,776,000s) for standard and premium tier stores. If specified, retention period must be at least 1 hour (3600s)') c.argument('tags', arg_type=tags_type, help="Space-separated tags: key[=value] [key[=value] ...].") + c.argument('description', help='Description of the App Configuration snapshot.') with self.argument_context('appconfig snapshot show') as c: c.argument('fields', arg_type=snapshot_fields_arg_type) diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/_snapshotmodels.py b/src/azure-cli/azure/cli/command_modules/appconfig/_snapshotmodels.py index 1ac5cde1217..21aa577770d 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/_snapshotmodels.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/_snapshotmodels.py @@ -25,7 +25,8 @@ class SnapshotQueryFields(Enum): TAGS = 0x0100 ETAG = 0x0200 RETENTION_PERIOD = 0x0400 - ALL = NAME | STATUS | FILTERS | COMPOSITION_TYPE | CREATED | EXPIRES | SIZE | ITEMS_COUNT | TAGS | ETAG | RETENTION_PERIOD + DESCRIPTION = 0x0800 + ALL = NAME | STATUS | FILTERS | COMPOSITION_TYPE | CREATED | EXPIRES | SIZE | ITEMS_COUNT | TAGS | ETAG | RETENTION_PERIOD | DESCRIPTION class Snapshot: @@ -54,6 +55,8 @@ class Snapshot: Dictionary of tags of the snapshot. :ivar int retention_period: Number of seconds for which an archived snapshot will be kept before being deleted. + :ivar str description: + Description of the snapshot. ''' def __init__(self, @@ -68,6 +71,7 @@ def __init__(self, items_count=None, tags=None, retention_period=None, + description=None, ): self.name = name @@ -81,6 +85,7 @@ def __init__(self, self.items_count = items_count self.tags = tags self.retention_period = retention_period + self.description = description def __str__(self): return "\nEtag: " + self.etag + \ @@ -93,7 +98,8 @@ def __str__(self): "\nSize: " + str(self.size) + \ "\nItem count: " + str(self.items_count) + \ "\nTags: " + (str(self.tags) if self.tags else '{}') + \ - "\nRetention Period: " + str(self.retention_period) + "\nRetention Period: " + str(self.retention_period) + \ + "\nDescription: " + (self.description if self.description else '') @classmethod def from_configuration_snapshot(cls, config_snapshot): @@ -108,7 +114,8 @@ def from_configuration_snapshot(cls, config_snapshot): size=config_snapshot.size, items_count=config_snapshot.items_count, tags=config_snapshot.tags, - retention_period=config_snapshot.retention_period + retention_period=config_snapshot.retention_period, + description=getattr(config_snapshot, 'description', None) ) diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/keyvalue.py b/src/azure-cli/azure/cli/command_modules/appconfig/keyvalue.py index 04538a3fd82..0fe37f77246 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/keyvalue.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/keyvalue.py @@ -474,6 +474,7 @@ def set_key(cmd, content_type=None, tags=None, value=None, + description=None, yes=False, connection_string=None, auth_mode="key", @@ -520,10 +521,12 @@ def set_key(cmd, label=label, value="" if value is None else value, content_type="" if content_type is None else content_type, - tags=tags) + tags=tags, + description=description) else: value = retrieved_kv.value if value is None else value content_type = retrieved_kv.content_type if content_type is None else content_type + description = retrieved_kv.description if description is None else description if is_json_content_type(content_type): try: # Ensure that provided value is valid JSON and strip comments if needed. @@ -536,14 +539,16 @@ def set_key(cmd, content_type=content_type, tags=retrieved_kv.tags if tags is None else tags, read_only=retrieved_kv.read_only, - etag=retrieved_kv.etag) + etag=retrieved_kv.etag, + description=description) verification_kv = { "key": set_kv.key, "label": set_kv.label, "content_type": set_kv.content_type, "value": set_kv.value, - "tags": set_kv.tags + "tags": set_kv.tags, + "description": set_kv.description } entry = json.dumps(verification_kv, indent=2, sort_keys=True, ensure_ascii=False) @@ -576,6 +581,7 @@ def set_keyvault(cmd, name=None, label=None, tags=None, + description=None, yes=False, connection_string=None, auth_mode="key", @@ -608,22 +614,26 @@ def set_keyvault(cmd, label=label, value=keyvault_ref_value, content_type=KeyVaultConstants.KEYVAULT_CONTENT_TYPE, - tags=tags) + tags=tags, + description=description) else: + description = retrieved_kv.description if description is None else description set_kv = ConfigurationSetting(key=key, label=label, value=keyvault_ref_value, content_type=KeyVaultConstants.KEYVAULT_CONTENT_TYPE, tags=retrieved_kv.tags if tags is None else tags, read_only=retrieved_kv.read_only, - etag=retrieved_kv.etag) + etag=retrieved_kv.etag, + description=description) verification_kv = { "key": set_kv.key, "label": set_kv.label, "content_type": set_kv.content_type, "value": set_kv.value, - "tags": set_kv.tags + "tags": set_kv.tags, + "description": set_kv.description } entry = json.dumps(verification_kv, indent=2, sort_keys=True, ensure_ascii=False) confirmation_message = "Are you sure you want to set the keyvault reference: \n" + entry + "\n" @@ -655,6 +665,7 @@ def set_snapshot_reference(cmd, name=None, label=None, tags=None, + description=None, yes=False, connection_string=None, auth_mode="key", @@ -687,22 +698,26 @@ def set_snapshot_reference(cmd, label=label, value=snapshot_ref_value, content_type=SnapshotReferenceConstants.SNAPSHOT_REFERENCE_CONTENT_TYPE, - tags=tags) + tags=tags, + description=description) else: + description = retrieved_kv.description if description is None else description set_kv = ConfigurationSetting(key=key, label=label, value=snapshot_ref_value, content_type=SnapshotReferenceConstants.SNAPSHOT_REFERENCE_CONTENT_TYPE, tags=retrieved_kv.tags if tags is None else tags, read_only=retrieved_kv.read_only, - etag=retrieved_kv.etag) + etag=retrieved_kv.etag, + description=description) verification_kv = { "key": set_kv.key, "label": set_kv.label, "content_type": set_kv.content_type, "value": set_kv.value, - "tags": set_kv.tags + "tags": set_kv.tags, + "description": set_kv.description } entry = json.dumps(verification_kv, indent=2, sort_keys=True, ensure_ascii=False) confirmation_message = "Are you sure you want to set the snapshot reference: \n" + entry + "\n" diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/snapshot.py b/src/azure-cli/azure/cli/command_modules/appconfig/snapshot.py index 8829e95f8a4..2d0c7b458e4 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/snapshot.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/snapshot.py @@ -26,7 +26,8 @@ def create_snapshot(cmd, endpoint=None, retention_period=None, composition_type=None, - tags=None): + tags=None, + description=None): client = get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint) @@ -44,7 +45,8 @@ def create_snapshot(cmd, configurationSettingsFilters, composition_type=composition_type, retention_period=retention_period, - tags=tags) + tags=tags, + description=description) # Poll snapshot creation status while config_snapshot_poller.status() != ProvisioningStatus.SUCCEEDED: diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_kv_commands.py b/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_kv_commands.py index c784dc065e5..5eada1d55af 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_kv_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_kv_commands.py @@ -311,6 +311,37 @@ def test_azconfig_kv(self, resource_group, location): with self.assertRaisesRegex(MutuallyExclusiveArgumentError, "The '--dry-run' and '--yes' options cannot be specified together."): self.cmd('appconfig kv restore --datetime "2019-05-01T11:24:12Z" --endpoint {endpoint} --auth-mode login --key {key} --label {label} --dry-run -y') + # Description property: set with --description, verify it is returned, preserved on edit, and overwritten + self.kwargs.update({ + 'key': "DescKey", + 'value': "Red", + 'description': "The theme color" + }) + self.cmd('appconfig kv set --endpoint {endpoint} --auth-mode login --key {key} --value {value} --description "{description}" -y', + checks=[self.check('key', "DescKey"), + self.check('value', "Red"), + self.check('description', "The theme color")]) + + # Description is returned by show + self.cmd('appconfig kv show --endpoint {endpoint} --auth-mode login --key {key}', + checks=[self.check('description', "The theme color")]) + + # Description is selectable via --fields + partial_kv = self.cmd('appconfig kv list --endpoint {endpoint} --auth-mode login --key {key} --fields description').get_output_in_json() + self.assertEqual(partial_kv[0]['description'], "The theme color") + self.assertNotIn('value', partial_kv[0]) + + # Updating another property without --description preserves the existing description + self.kwargs.update({'value': "Green"}) + self.cmd('appconfig kv set --endpoint {endpoint} --auth-mode login --key {key} --value {value} -y', + checks=[self.check('value', "Green"), + self.check('description', "The theme color")]) + + # Explicitly updating the description overwrites it + self.kwargs.update({'description': "The updated theme color"}) + self.cmd('appconfig kv set --endpoint {endpoint} --auth-mode login --key {key} --description "{description}" -y', + checks=[self.check('description', "The updated theme color")]) + @AllowLargeResponse() @ResourceGroupPreparer() diff --git a/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_snapshot_commands.py b/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_snapshot_commands.py index 92cc353d2c6..d876ebf8ba7 100644 --- a/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_snapshot_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appconfig/tests/latest/test_appconfig_snapshot_commands.py @@ -93,24 +93,32 @@ def test_azconfig_snapshot_mgmt(self, resource_group, location): 'retention_period': retention_period }) - self.cmd('appconfig snapshot create --endpoint {endpoint} --auth-mode login --snapshot-name {snapshot_name} --filters {filter} --retention-period {retention_period} --composition-type key_label --tags tag1=value1', + snapshot_description = "Snapshot of Test key-values" + self.kwargs.update({ + 'snapshot_description': snapshot_description + }) + + self.cmd('appconfig snapshot create --endpoint {endpoint} --auth-mode login --snapshot-name {snapshot_name} --filters {filter} --retention-period {retention_period} --composition-type key_label --tags tag1=value1 --description "{snapshot_description}"', checks=[self.check('itemsCount', 2), - self.check('status', 'ready')]) + self.check('status', 'ready'), + self.check('description', snapshot_description)]) # Test showing created snapshot - created_snapshot = self.cmd('appconfig snapshot show --endpoint {endpoint} --auth-mode login --snapshot-name {snapshot_name} --fields name status items_count filters').get_output_in_json() + created_snapshot = self.cmd('appconfig snapshot show --endpoint {endpoint} --auth-mode login --snapshot-name {snapshot_name} --fields name status items_count filters description').get_output_in_json() self.assertEqual(created_snapshot['items_count'], 2) self.check(created_snapshot['status'], 'ready') self.assertDictEqual(created_snapshot['filters'][0], filter_dict) + self.assertEqual(created_snapshot['description'], snapshot_description) self.assertRaises(KeyError, lambda: created_snapshot['created']) # Test listing snapshots - created_snapshots = self.cmd('appconfig snapshot list --snapshot-name {snapshot_name} --endpoint {endpoint} --auth-mode login --fields name status items_count filters').get_output_in_json() + created_snapshots = self.cmd('appconfig snapshot list --snapshot-name {snapshot_name} --endpoint {endpoint} --auth-mode login --fields name status items_count filters description').get_output_in_json() self.assertEqual(created_snapshots[0]['items_count'], 2) self.assertEqual(created_snapshots[0]['status'], 'ready') self.assertDictEqual(created_snapshots[0]['filters'][0], filter_dict) + self.assertEqual(created_snapshots[0]['description'], snapshot_description) # Test snapshot archive archived_snapshot = self.cmd('appconfig snapshot archive --endpoint {endpoint} --auth-mode login --snapshot-name {snapshot_name}').get_output_in_json()