Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def cf_replicas(cli_ctx, *_):
return get_appconfig_service_client(cli_ctx).replicas


def cf_nsp_configurations(cli_ctx, *_):
return get_appconfig_service_client(cli_ctx).network_security_perimeter_configurations


def cf_configstore_operations(cli_ctx, *_):
try:
return get_appconfig_service_client(cli_ctx).operations
Expand Down
14 changes: 14 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appconfig/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def configstore_snapshot_output_format(result):
return _output_format(result, _snapshot_output_format_group)


def configstore_nsp_output_format(result):
return _output_format(result, _configstore_nsp_format_group)


def _output_format(result, format_group):
if 'value' in result and isinstance(result['value'], list):
result = result['value']
Expand Down Expand Up @@ -164,6 +168,16 @@ def _featurefilter_entry_format_group(item):
])


def _configstore_nsp_format_group(item):
return OrderedDict([
('PERIMETER ID', _get_value(item, 'properties', 'networkSecurityPerimeter', 'id')),
('PROFILE NAME', _get_value(item, 'properties', 'profile', 'name')),
('ACCESS MODE', _get_value(item, 'properties', 'resourceAssociation', 'accessMode')),
('ACCESS RULES VERSION', _get_value(item, 'properties', 'profile', 'accessRulesVersion')),
('DIAGNOSTIC SETTINGS VERSION', _get_value(item, 'properties', 'profile', 'diagnosticSettingsVersion'))
])


def _format_datetime(date_string):
from dateutil.parser import parse
try:
Expand Down
32 changes: 32 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appconfig/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,38 @@
short-summary: Manage snapshots associated with an app configuration store.
"""

helps['appconfig network-security-perimeter-configuration'] = """
type: group
short-summary: Manage network security perimeter configurations for an App Configuration store.
"""

helps['appconfig network-security-perimeter-configuration list'] = """
type: command
short-summary: List all network security perimeter configurations for an App Configuration store.
examples:
- name: List all network security perimeter configurations for an App Configuration store.
text:
az appconfig network-security-perimeter-configuration list --store-name MyAppConfiguration -g MyResourceGroup
"""

helps['appconfig network-security-perimeter-configuration show'] = """
type: command
short-summary: Show a specific network security perimeter configuration for an App Configuration store.
examples:
- name: Show a network security perimeter configuration by name.
text:
az appconfig network-security-perimeter-configuration show --store-name MyAppConfiguration -g MyResourceGroup --name MyNspConfigurationName
"""

helps['appconfig network-security-perimeter-configuration reconcile'] = """
type: command
short-summary: Force a refresh of the specified network security perimeter configuration for an App Configuration store.
examples:
- name: Reconcile a network security perimeter configuration.
text:
az appconfig network-security-perimeter-configuration reconcile --store-name MyAppConfiguration -g MyResourceGroup --name MyNspConfigurationName
"""

helps['appconfig snapshot create'] = """
type: command
short-summary: Create an app configuration snapshot.
Expand Down
26 changes: 23 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appconfig/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
validate_resolve_keyvault, validate_export_profile, validate_import_profile,
validate_strict_import, validate_export_as_reference, validate_snapshot_filters,
validate_snapshot_export, validate_snapshot_import, validate_tag_filters,
validate_import_tag_filters, validate_dry_run, validate_kv_revision_retention_period)
validate_import_tag_filters, validate_dry_run, validate_kv_revision_retention_period,
validate_public_network_args)


def load_arguments(self, _):
Expand Down Expand Up @@ -180,7 +181,12 @@ def load_arguments(self, _):
c.argument('assign_identity', arg_type=identities_arg_type,
help='Space-separated list of managed identities to be assigned. Use "[system]" to refer to system-assigned managed identity or a resource ID to refer to user-assigned managed identity. If this argument is provided without any value, system-assigned managed identity will be assigned by default. If this argument is not provided, no managed identities will be assigned to this App Configuration store.')
c.argument('enable_public_network', options_list=['--enable-public-network', '-e'], arg_type=get_three_state_flag(),
help='When true, requests coming from public networks have permission to access this store while private endpoint is enabled. When false, only requests made through Private Links can reach this store.')
help='When true, requests coming from public networks have permission to access this store while private endpoint is enabled. When false, only requests made through Private Links can reach this store.',
deprecate_info=c.deprecate(redirect='--public-network-access', hide=False))
c.argument('public_network_access', options_list=['--public-network-access'],
arg_type=get_enum_type(['Disabled', 'Enabled', 'SecuredByPerimeter']),
help='Control permission for data plane traffic coming from public networks. Possible values include: Disabled, Enabled, SecuredByPerimeter. NOTE: The SecuredByPerimeter mode is currently in preview.',
validator=validate_public_network_args)
c.argument('disable_local_auth', arg_type=get_three_state_flag(), help='Disable all authentication methods other than AAD authentication.')
c.argument('retention_days', arg_type=retention_days_arg_type)
c.argument('enable_purge_protection', options_list=['--enable-purge-protection', '-p'], arg_type=get_three_state_flag(), help='Property specifying whether protection against purge is enabled for this App Configuration store. Setting this property to true activates protection against purge for this App Configuration store and its contents. Enabling this functionality is irreversible.')
Expand All @@ -197,7 +203,12 @@ def load_arguments(self, _):
c.argument('sku', help='The sku of the App Configuration store', arg_type=get_enum_type(['Free', 'Developer', 'Premium', 'Standard']))
c.argument('tags', arg_type=tags_type)
c.argument('enable_public_network', options_list=['--enable-public-network', '-e'], arg_type=get_three_state_flag(),
help='When true, requests coming from public networks have permission to access this store while private endpoint is enabled. When false, only requests made through Private Links can reach this store.')
help='When true, requests coming from public networks have permission to access this store while private endpoint is enabled. When false, only requests made through Private Links can reach this store.',
deprecate_info=c.deprecate(redirect='--public-network-access', hide=False))
c.argument('public_network_access', options_list=['--public-network-access'],
arg_type=get_enum_type(['Disabled', 'Enabled', 'SecuredByPerimeter']),
help='Control permission for data plane traffic coming from public networks. Possible values include: Disabled, Enabled, SecuredByPerimeter. NOTE: The SecuredByPerimeter mode is currently in preview.',
validator=validate_public_network_args)
c.argument('disable_local_auth', arg_type=get_three_state_flag(), help='Disable all authentication methods other than AAD authentication.')
c.argument('enable_purge_protection', options_list=['--enable-purge-protection', '-p'], arg_type=get_three_state_flag(), help='Property specifying whether protection against purge is enabled for this App Configuration store. Setting this property to true activates protection against purge for this App Configuration store and its contents. Enabling this functionality is irreversible.')
c.argument('arm_auth_mode', arg_type=arm_auth_mode_arg_type)
Expand Down Expand Up @@ -473,3 +484,12 @@ def load_arguments(self, _):
c.argument('snapshot_name', options_list=['--snapshot-name', '-s'], help='If no name specified, return all snapshots by default. Support star sign as filters, for instance abc* means snapshots with abc as prefix to the name.')
c.argument('status', arg_type=snapshot_status_arg_type)
c.argument('fields', arg_type=snapshot_fields_arg_type)

with self.argument_context('appconfig network-security-perimeter-configuration') as c:
c.argument('store_name', arg_type=store_name_arg_type)

with self.argument_context('appconfig network-security-perimeter-configuration show') as c:
c.argument('name', options_list=['--name', '-n'], help='Name of the network security perimeter configuration to show.', required=True)

with self.argument_context('appconfig network-security-perimeter-configuration reconcile') as c:
c.argument('name', options_list=['--name', '-n'], help='Name of the network security perimeter configuration to reconcile.', required=True)
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,14 @@ def validate_snapshot_import(namespace):
raise MutuallyExclusiveArgumentError("'--src-snapshot' cannot be specified with '--src-key', '--src-label', or '--skip-features' arguments.")


def validate_public_network_args(namespace):
if namespace.enable_public_network is not None and namespace.public_network_access is not None:
raise MutuallyExclusiveArgumentError("Cannot specify both '--enable-public-network' and '--public-network-access'. "
"Please use '--public-network-access' as '--enable-public-network' has been deprecated.")
if namespace.public_network_access is not None and namespace.public_network_access.lower() == 'securedbyperimeter':
logger.warning("The 'SecuredByPerimeter' value is currently in preview.")


def validate_sku(namespace):
if namespace.sku.lower() == 'free':
if (namespace.enable_purge_protection or namespace.retention_days or namespace.replica_name or namespace.replica_location or namespace.no_replica or namespace.enable_arm_private_network_access): # pylint: disable=too-many-boolean-expressions
Expand Down
17 changes: 15 additions & 2 deletions src/azure-cli/azure/cli/command_modules/appconfig/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from azure.cli.core.commands import CliCommandType

from ._client_factory import cf_configstore, cf_configstore_operations, cf_replicas
from ._client_factory import cf_configstore, cf_configstore_operations, cf_replicas, cf_nsp_configurations
from ._format import (configstore_credential_format,
configstore_identity_format,
configstore_output_format,
Expand All @@ -16,7 +16,8 @@
featurefilter_entry_format,
deleted_configstore_output_format,
configstore_replica_output_format,
configstore_snapshot_output_format)
configstore_snapshot_output_format,
configstore_nsp_output_format)


def load_command_table(self, _):
Expand Down Expand Up @@ -56,6 +57,12 @@ def load_command_table(self, _):
table_transformer=configstore_snapshot_output_format
)

configstore_nsp_util = CliCommandType(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every other command seems to have table_transformer specified. How come nsp doesn't need it?

@ChristineWanjau Christine Wanjau (ChristineWanjau) May 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! The table_transformer controls how a command's output is rendered when the user passes -o table. We should add it for nsp as well. I am not sure what fields to have in the nsp table_transformer. There are a couple of fields which are nested together within the NSP configuration response. https://review.learn.microsoft.com/en-us/rest/api/documentation-preview/network-security-perimeter-configurations?view=azure-rest-preview&branch=preview%2FAzure%2Fazure-rest-api-specs%2Fpr%2F38653%2Fbuild%2F6138442%2Fattempt%2F1 .

The second part has the table_transformer specified.
image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a table_transformer format for nsp. Please let me know what other fields we should have

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you paste the output with the nsp transformer in use? I can't access your image for some reason.

@jimmyca15 Jimmy Campbell (jimmyca15) May 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it to load. It does look like a lot of columns. After some thought I think maybe we can reduce it to the below set of columns:

perimeter id
profile name
access mode
access rules version
diagnostic setting version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been updated :)

operations_tmpl='azure.cli.command_modules.appconfig.network_security_perimeter#{}',
table_transformer=configstore_nsp_output_format,
client_factory=cf_nsp_configurations
)

def get_custom_sdk(custom_module, client_factory, table_transformer):
"""Returns a CliCommandType instance with specified operation template based on the given custom module name.
This is useful when the command is not defined in the default 'custom' module but instead in a module under
Expand Down Expand Up @@ -141,3 +148,9 @@ def get_custom_sdk(custom_module, client_factory, table_transformer):
g.command('list', 'list_snapshots')
g.command('archive', 'archive_snapshot')
g.command('recover', 'recover_snapshot')

# NSP Commands
with self.command_group('appconfig network-security-perimeter-configuration', configstore_nsp_util, is_preview=True) as g:
g.command('list', 'list_nsp_configurations')
g.show_command('show', 'show_nsp_configuration')
g.command('reconcile', 'reconcile_nsp_configuration')
14 changes: 11 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appconfig/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from azure.cli.core.commands.progress import IndeterminateStandardOut
from azure.cli.core.util import user_confirmation
from azure.core.exceptions import ResourceNotFoundError
from azure.cli.core.azclierror import RequiredArgumentMissingError
from azure.cli.core.azclierror import MutuallyExclusiveArgumentError, RequiredArgumentMissingError
from azure.mgmt.appconfiguration.models import (ConfigurationStoreUpdateParameters,
ConfigurationStore,
Sku,
Expand Down Expand Up @@ -48,6 +48,7 @@ def create_configstore(cmd, # pylint: disable=too-many-locals
tags=None,
assign_identity=None,
enable_public_network=None,
public_network_access=None,
disable_local_auth=None,
retention_days=None,
enable_purge_protection=None,
Expand All @@ -62,7 +63,10 @@ def create_configstore(cmd, # pylint: disable=too-many-locals
if assign_identity is not None and not assign_identity:
assign_identity = [SYSTEM_ASSIGNED_IDENTITY]

public_network_access = None
if public_network_access is not None and enable_public_network is not None:
raise MutuallyExclusiveArgumentError("Cannot specify both '--enable-public-network' and '--public-network-access'. "

@jimmyca15 Jimmy Campbell (jimmyca15) May 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of an internal error. We've escaped the context of the CLI user's arguments. The 'public' error is the one in _validators.py. So the error message here doesn't need to be as detailed. In fact, it should be something more suitable for internal errors. Something like

"Only one of public_network_access and enable_public_network can be provided."

"Please use '--public-network-access' as '--enable-public-network' has been deprecated.")

if enable_public_network is not None:
public_network_access = PublicNetworkAccess.ENABLED if enable_public_network else PublicNetworkAccess.DISABLED

Expand Down Expand Up @@ -193,6 +197,7 @@ def update_configstore(cmd, # pylint: disable=too-many-locals
encryption_key_version=None,
identity_client_id=None,
enable_public_network=None,
public_network_access=None,
disable_local_auth=None,
enable_purge_protection=None,
arm_auth_mode=None,
Expand All @@ -204,7 +209,10 @@ def update_configstore(cmd, # pylint: disable=too-many-locals
if resource_group_name is None:
resource_group_name, _ = resolve_store_metadata(cmd, name)

public_network_access = None
if public_network_access is not None and enable_public_network is not None:
raise MutuallyExclusiveArgumentError("Cannot specify both '--enable-public-network' and '--public-network-access'. "
"Please use '--public-network-access' as '--enable-public-network' has been deprecated.")

if enable_public_network is not None:
public_network_access = PublicNetworkAccess.ENABLED if enable_public_network else PublicNetworkAccess.DISABLED

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# pylint: disable=line-too-long

from azure.core.exceptions import ResourceNotFoundError

from ._utils import resolve_store_metadata


def list_nsp_configurations(cmd, client, store_name, resource_group_name=None):
if resource_group_name is None:
resource_group_name, _ = resolve_store_metadata(cmd, store_name)
return client.list_by_configuration_store(resource_group_name=resource_group_name, config_store_name=store_name)


def show_nsp_configuration(cmd, client, store_name, name, resource_group_name=None):
if resource_group_name is None:
resource_group_name, _ = resolve_store_metadata(cmd, store_name)
try:
return client.get(
resource_group_name=resource_group_name,
config_store_name=store_name,
network_security_perimeter_configuration_name=name
)
except ResourceNotFoundError:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that when we do a show for a configuration store, we let this error go unhandled (link).

So is error handling missing there, or is this unnecessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the resource is missing, the SDK raises ResourceNotFoundError with the message "Operation returned an invalid status 'Not Found'", which doesn't tell the user which resource was not found its a bit generic. We catch and re-raise with a message that includes the NSP configuration name, so the error is actionable.
image

For a store, we catch it here in the resolve_store_metadata function

raise ResourceNotFoundError("Failed to find the App Configuration store '{}'.".format(config_store_name))
and raise it with a better error message

Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That makes sense then. Thanks.

raise ResourceNotFoundError(
"The network security perimeter configuration '{}' for App Configuration '{}' was not found.".format(
name, store_name))


def reconcile_nsp_configuration(cmd, client, store_name, name, resource_group_name=None):
if resource_group_name is None:
resource_group_name, _ = resolve_store_metadata(cmd, store_name)
try:
return client.begin_reconcile(
resource_group_name=resource_group_name,
config_store_name=store_name,
network_security_perimeter_configuration_name=name
)
except ResourceNotFoundError:
raise ResourceNotFoundError(
"The network security perimeter configuration '{}' for App Configuration '{}' was not found.".format(
name, store_name))
Loading