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
8 changes: 8 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appconfig/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ def construct_connection_string(cmd, config_store_name):
raise CLIError('Cannot find a read write access key for the App Configuration {}'.format(config_store_name))


def get_store_name_from_endpoint(endpoint):
if endpoint:
return endpoint.split("//")[1].split('.')[0]
return None


def resolve_store_metadata(cmd, config_store_name):
resource_group = None
endpoint = None
Expand Down Expand Up @@ -212,6 +218,8 @@ def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint)
token_audience = None
if hasattr(current_cloud.endpoints, "appconfig_auth_token_audience"):
token_audience = current_cloud.endpoints.appconfig_auth_token_audience
else:
token_audience = endpoint
Comment thread
mrm9084 marked this conversation as resolved.

try:
azconfig_client = AzureAppConfigurationClient(credential=AppConfigurationCliCredential(cred, token_audience),
Expand Down
16 changes: 13 additions & 3 deletions src/azure-cli/azure/cli/command_modules/appconfig/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
from ._utils import (get_appconfig_data_client,
prep_filter_for_url_encoding,
validate_feature_flag_name,
resolve_store_metadata)
resolve_store_metadata,
get_store_name_from_connection_string,
get_store_name_from_endpoint)
from ._featuremodels import (map_keyvalue_to_featureflag,
map_keyvalue_to_featureflagvalue,
FeatureFilter,
Expand All @@ -43,14 +45,22 @@
# Feature commands #


def warn_if_app_insights_not_set(cmd, store_name):
def warn_if_app_insights_not_set(cmd, store_name=None, connection_string=None, endpoint=None):
"""
Check if Application Insights resource is set for the App Configuration store.
Emits a warning if not set or if the check cannot be completed.
"""
from ._client_factory import cf_configstore

try:
# When the store name is not provided directly (e.g. the command used --endpoint or
# --connection-string instead of --name), derive it so the App Insights link can be
# resolved via the management plane.
if not store_name:
store_name = (get_store_name_from_connection_string(connection_string)
if connection_string
else get_store_name_from_endpoint(endpoint))

resource_group_name, _ = resolve_store_metadata(cmd, store_name)
configstore_client = cf_configstore(cmd.cli_ctx)
store = configstore_client.get(resource_group_name, store_name)
Expand Down Expand Up @@ -118,7 +128,7 @@ def set_feature(cmd,
if telemetry_enabled is not None:
default_value[FeatureFlagConstants.TELEMETRY] = {FeatureFlagConstants.ENABLED: telemetry_enabled}
if telemetry_enabled:
warn_if_app_insights_not_set(cmd, name)
warn_if_app_insights_not_set(cmd, name, connection_string, endpoint)

azconfig_client = get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
from azure.cli.testsdk.scenario_tests.utilities import is_json_payload
from azure.cli.core.util import shell_safe_json_parse

def create_config_store(test, kwargs):
def create_config_store(test, kwargs, disable_local_auth=False):
if 'retention_days' not in kwargs:
kwargs.update({
'retention_days': 1
})
test.cmd('appconfig create -n {config_store_name} -g {rg} -l {rg_loc} --sku {sku} --retention-days {retention_days}')
command = 'appconfig create -n {config_store_name} -g {rg} -l {rg_loc} --sku {sku} --retention-days {retention_days}'
if disable_local_auth:
command += ' --disable-local-auth true'
test.cmd(command)


def _get_local_test_resource_prefix():
Expand All @@ -25,6 +28,68 @@ def get_resource_name_prefix(prefix):
resource_prefix = _get_local_test_resource_prefix()
return prefix if resource_prefix is None else resource_prefix + prefix

def get_test_resource_group():
# Data-plane tests use Microsoft Entra ID (--auth-mode login) against a store whose local auth
# is disabled. The recording principal must already hold "App Configuration Data Owner" at a
# scope covering the store, so tests target a fixed resource group with that standing role
# instead of an ephemeral @ResourceGroupPreparer group. Override via AZURE_CLI_APPCONFIG_TEST_RG.
return os.environ.get("AZURE_CLI_APPCONFIG_TEST_RG", "mametcal-python")


def _case_insensitive_query_matcher(r1, r2):
""" Ensure method, path, and query parameters match.

Query parameter names are case-insensitive (e.g. OData '$select' vs '$Select'),
so normalize the keys before comparing to avoid spurious cassette mismatches
caused by SDK serialization differences across versions.
"""
from urllib.parse import urlparse, parse_qs

url1 = urlparse(r1.uri)
url2 = urlparse(r2.uri)

q1 = {k.lower(): v for k, v in parse_qs(url1.query).items()}
q2 = {k.lower(): v for k, v in parse_qs(url2.query).items()}
shared_keys = set(q1.keys()).intersection(set(q2.keys()))

if len(shared_keys) != len(q1) or len(shared_keys) != len(q2):
return False

for key in shared_keys:
if q1[key][0].lower() != q2[key][0].lower():
return False

return True


def register_appconfig_query_matcher(test):
""" Register the App Configuration case-insensitive query matcher on the test's VCR instance. """
test.vcr.register_matcher('query', _case_insensitive_query_matcher)


class OperationLocationSanitizer(RecordingProcessor):
""" Scrub the store name from the 'operation-location' response header.

App Configuration snapshot long-running operations return the store endpoint in the
'operation-location' header, which the SDK follows to poll for completion. The base
GeneralNameReplacer only sanitizes the 'location' and 'azure-asyncoperation' headers,
so without this the real store name leaks into recordings and breaks playback (the
poller targets the un-sanitized host).
"""

def __init__(self, name_replacer):
self._name_replacer = name_replacer

def process_response(self, response):
for old, new in self._name_replacer.names_name:
self._name_replacer.replace_header(response, 'operation-location', old, new)
return response


def register_appconfig_recording_processors(test):
""" Register App Configuration-specific recording processors on the test. """
test.recording_processors.append(OperationLocationSanitizer(test.name_replacer))

class CredentialResponseSanitizer(RecordingProcessor):
def process_response(self, response):
if is_json_payload(response):
Expand Down
Loading
Loading