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
1 change: 1 addition & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Release History

**App Config**

* `az appconfig enhanced-feature-flag`: Add command group to manage enhanced feature flags using the new data-plane `FeatureFlagClient`
* Sanitize resource group name in data-plane test recordings (#33906)

**App Service**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class FeatureFlagConstants:
REQUIREMENT_TYPE = "requirement_type"
DISPLAY_NAME = "display_name"
NAME = "name"
LABEL = "label"
TAGS = "tags"
LAST_MODIFIED = "last_modified"
FILTER_PARAMETERS = "parameters"
ALLOCATION = "allocation"
TELEMETRY = "telemetry"
Expand Down
20 changes: 20 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 @@ -34,6 +34,10 @@ def featureflag_entry_format(result):
return _output_format(result, _featureflag_entry_format_group)


def enhanced_featureflag_entry_format(result):
return _output_format(result, _enhanced_featureflag_entry_format_group)


def featurefilter_entry_format(result):
return _output_format(result, _featurefilter_entry_format_group)

Expand Down Expand Up @@ -146,6 +150,22 @@ def _featureflag_entry_format_group(item):
])


def _enhanced_featureflag_entry_format_group(item):

return OrderedDict([
('NAME', _get_value(item, 'name')),
('ENABLED', _get_value(item, 'enabled')),
('LABEL', _get_value(item, 'label')),
('DESCRIPTION', _get_value(item, 'description')),
('LAST MODIFIED', _format_datetime(_get_value_by_names(item, ['lastModified', 'last_modified']))),
('TAGS', _get_value(item, 'tags')),
('CONDITIONS', _get_value(item, 'conditions')),
('VARIANTS', _get_value(item, 'variants')),
('ALLOCATION', _get_value(item, 'allocation')),
('TELEMETRY', _get_value(item, 'telemetry'))
])


def _snapshot_output_format_group(item):

return OrderedDict([
Expand Down
104 changes: 104 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 @@ -612,6 +612,110 @@
az appconfig feature disable -n MyAppConfiguration --key .appconfig.featureflag/MyApp1:Beta
"""

helps['appconfig enhanced-feature-flag'] = """
type: group
short-summary: Manage enhanced feature flags stored in an App Configuration store.
"""

helps['appconfig enhanced-feature-flag set'] = """
type: command
short-summary: Set an enhanced feature flag.
examples:
- name: Set an enhanced feature flag with label MyLabel.
text:
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --label MyLabel
- name: Set an enhanced feature flag with null label using connection string and set a description.
text:
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"
- name: Set an enhanced feature flag using your 'az login' credentials.
text:
az appconfig enhanced-feature-flag set --endpoint https://myappconfiguration.azconfig.io --feature color --label MyLabel --auth-mode login
- name: Set an enhanced feature flag with tags "tag1=value1" and "tag2=value2".
text:
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --tags tag1=value1 tag2=value2
- name: Set an enhanced feature flag with telemetry enabled.
text:
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --telemetry-enabled
- name: Disable telemetry on an enhanced feature flag.
text:
az appconfig enhanced-feature-flag set -n MyAppConfiguration --feature color --telemetry-enabled false
"""

helps['appconfig enhanced-feature-flag delete'] = """
type: command
short-summary: Delete an enhanced feature flag.
examples:
- name: Delete an enhanced feature flag using App Configuration store name without confirmation.
text:
az appconfig enhanced-feature-flag delete -n MyAppConfiguration --feature color --label MyLabel --yes
- name: Delete an enhanced feature flag using connection string.
text:
az appconfig enhanced-feature-flag delete --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --label MyLabel
- name: Delete an enhanced feature flag using App Configuration store endpoint and your 'az login' credentials.
text:
az appconfig enhanced-feature-flag delete --endpoint https://myappconfiguration.azconfig.io --feature color --auth-mode login
"""

helps['appconfig enhanced-feature-flag show'] = """
type: command
short-summary: Show all attributes of an enhanced feature flag.
examples:
- name: Show an enhanced feature flag using App Configuration store name with a specific label.
text:
az appconfig enhanced-feature-flag show -n MyAppConfiguration --feature color --label MyLabel
- name: Show an enhanced feature flag using connection string and field filters.
text:
az appconfig enhanced-feature-flag show --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --fields name enabled conditions
- name: Show an enhanced feature flag using App Configuration store endpoint and your 'az login' credentials.
text:
az appconfig enhanced-feature-flag show --endpoint https://myappconfiguration.azconfig.io --feature color --auth-mode login
"""

helps['appconfig enhanced-feature-flag list'] = """
type: command
short-summary: List enhanced feature flags.
examples:
- name: List all enhanced feature flags.
text:
az appconfig enhanced-feature-flag list -n MyAppConfiguration
- name: List a specific feature for any label starting with v1. using connection string.
text:
az appconfig enhanced-feature-flag list --feature color --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --label v1.*
- name: List all features with any labels and query only name, enabled and conditions.
text:
az appconfig enhanced-feature-flag list --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --fields name enabled conditions
- name: List 150 enhanced feature flags with any labels.
text:
az appconfig enhanced-feature-flag list --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --top 150
- name: List all enhanced feature flags with specific tags.
text:
az appconfig enhanced-feature-flag list -n MyAppConfiguration --tags tag1=value1 tag2=value2
"""

helps['appconfig enhanced-feature-flag enable'] = """
type: command
short-summary: Enable an enhanced feature flag to turn it ON for use.
examples:
- name: Enable an enhanced feature flag using App Configuration store name.
text:
az appconfig enhanced-feature-flag enable -n MyAppConfiguration --feature color --label test
- name: Force enabling an enhanced feature flag using connection string.
text:
az appconfig enhanced-feature-flag enable --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --label test --yes
"""

helps['appconfig enhanced-feature-flag disable'] = """
type: command
short-summary: Disable an enhanced feature flag to turn it OFF for use.
examples:
- name: Disable an enhanced feature flag using App Configuration store name.
text:
az appconfig enhanced-feature-flag disable -n MyAppConfiguration --feature color --label test
- name: Force disabling an enhanced feature flag using connection string.
text:
az appconfig enhanced-feature-flag disable --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --label test --yes
"""

helps['appconfig feature filter'] = """
type: group
short-summary: Manage filters associated with feature flags stored in an App Configuration store.
Expand Down
30 changes: 29 additions & 1 deletion src/azure-cli/azure/cli/command_modules/appconfig/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
validate_connection_string, validate_datetime,
validate_export, validate_import,
validate_import_depth, validate_query_fields,
validate_feature_query_fields, validate_filter_parameters,
validate_feature_query_fields, validate_enhanced_feature_query_fields, validate_filter_parameters,
validate_separator, validate_secret_identifier,
validate_key, validate_feature, validate_feature_key,
validate_identity, validate_auth_mode, validate_snapshot_reference,
Expand All @@ -47,6 +47,12 @@ def load_arguments(self, _):
validator=validate_feature_query_fields,
arg_type=get_enum_type(['name', 'key', 'label', 'locked', 'last_modified', 'state', 'description', 'conditions'])
)
enhanced_feature_fields_arg_type = CLIArgumentType(
nargs='+',
help='Customize output fields for Enhanced Feature Flags.',
validator=validate_enhanced_feature_query_fields,
arg_type=get_enum_type(['name', 'enabled', 'label', 'description', 'conditions', 'variants', 'allocation', 'tags', 'last_modified'])
)
snapshot_fields_arg_type = CLIArgumentType(
nargs='+',
help='Customize output fields for Snapshots',
Expand Down Expand Up @@ -464,6 +470,28 @@ def load_arguments(self, _):
c.argument('label', help="If no label specified, display filters from the feature flag with null label by default.")
c.argument('all_', help="List all filters associated with a feature flag.")

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

with self.argument_context('appconfig enhanced-feature-flag set') as c:
c.argument('description', help='Description of the enhanced feature flag to be set.')
c.argument('requirement_type', arg_type=get_enum_type([FeatureFlagConstants.REQUIREMENT_TYPE_ALL, FeatureFlagConstants.REQUIREMENT_TYPE_ANY]),
help='Requirement type determines if filters should use "Any" or "All" logic when evaluating the state of an enhanced feature flag.')
c.argument('telemetry_enabled', arg_type=get_three_state_flag(), help='Enable or disable telemetry for the enhanced feature flag.')
c.argument('tags', arg_type=tags_type)

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

with self.argument_context('appconfig enhanced-feature-flag list') as c:
c.argument('feature', validator=validate_feature, help='Name of the enhanced feature flag to be listed. Support star sign as filters, for instance * means all enhanced feature flags and abc* means enhanced feature flags with abc as prefix.')
c.argument('label', help="If no label specified, list all labels. Support star sign as filters, for instance * means all labels and abc* means labels with abc as prefix.")
c.argument('fields', arg_type=enhanced_feature_fields_arg_type)
c.argument('all_', help="List all enhanced feature flags.")
c.argument('tags', arg_type=tags_arg_type, help="If no tags are specified, list all enhanced feature flags with any tags. Support space-separated tags: key[=value] [key[=value] ...].")

with self.argument_context('appconfig replica') as c:
c.argument('store_name', arg_type=store_name_arg_type)
c.argument('name', arg_type=replica_name_arg_type)
Expand Down
100 changes: 73 additions & 27 deletions src/azure-cli/azure/cli/command_modules/appconfig/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# pylint: disable=line-too-long
from knack.log import get_logger
from knack.util import CLIError
from azure.appconfiguration import AzureAppConfigurationClient
from azure.appconfiguration import AzureAppConfigurationClient, FeatureFlagClient
from azure.core.exceptions import HttpResponseError
from azure.core.credentials import AzureKeyCredential
from azure.cli.core.azclierror import (ValidationError,
Expand Down Expand Up @@ -177,15 +177,14 @@ def send(self, request, **kwargs): # pylint: disable=arguments-differ
return super().send(request, **kwargs)


def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint):
azconfig_client = None
def _get_data_plane_retry_policy():
# Configure retries with exponential backoff (factor=0.5) capped at 30 seconds,
# with an overall retry timeout of 100 seconds.
# We set retry count to a high number so the retry policy can continue retrying until
# the timeout is reached. The actual retry timing may vary, for example when the service
# returns a Retry-After header and the policy uses that delay instead of the exponential backoff.
retry_count = 9999
retry_policy = RetryPolicy(
return RetryPolicy(
retry_total=retry_count,
retry_connect=retry_count,
retry_read=retry_count,
Expand All @@ -195,6 +194,36 @@ def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint)
timeout=100 # seconds
)


def resolve_login_credential(cmd, name, endpoint):
if not endpoint:
try:
if name:
_, endpoint = resolve_store_metadata(cmd, name)
else:
raise RequiredArgumentMissingError("App Configuration endpoint or name should be provided if auth mode is 'login'.")
except Exception as ex:
raise CLIError(str(ex) + "\nYou may be able to resolve this issue by providing App Configuration endpoint instead of name.")

from azure.cli.core._profile import Profile
from azure.cli.core.cloud import get_active_cloud
from ._credential import AppConfigurationCliCredential
profile = Profile(cli_ctx=cmd.cli_ctx)
cred, _, _ = profile.get_login_credentials()

current_cloud = get_active_cloud(cmd.cli_ctx)
if hasattr(current_cloud.endpoints, "appconfig_auth_token_audience"):
token_audience = current_cloud.endpoints.appconfig_auth_token_audience
else:
token_audience = endpoint

return AppConfigurationCliCredential(cred, token_audience), endpoint


def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint):
azconfig_client = None
retry_policy = _get_data_plane_retry_policy()

if auth_mode == "anonymous":
try:
azconfig_client = AzureAppConfigurationClient(
Expand All @@ -217,30 +246,9 @@ def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint)
raise CLIError("Failed to initialize AzureAppConfigurationClient due to an exception: {}".format(str(ex)))

if auth_mode == "login":
if not endpoint:
try:
if name:
_, endpoint = resolve_store_metadata(cmd, name)
else:
raise RequiredArgumentMissingError("App Configuration endpoint or name should be provided if auth mode is 'login'.")
except Exception as ex:
raise CLIError(str(ex) + "\nYou may be able to resolve this issue by providing App Configuration endpoint instead of name.")

from azure.cli.core._profile import Profile
from azure.cli.core.cloud import get_active_cloud
from ._credential import AppConfigurationCliCredential
profile = Profile(cli_ctx=cmd.cli_ctx)
cred, _, _ = profile.get_login_credentials()

current_cloud = get_active_cloud(cmd.cli_ctx)
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

credential, endpoint = resolve_login_credential(cmd, name, endpoint)
try:
azconfig_client = AzureAppConfigurationClient(credential=AppConfigurationCliCredential(cred, token_audience),
azconfig_client = AzureAppConfigurationClient(credential=credential,
base_url=endpoint,
user_agent=HttpHeaders.USER_AGENT,
retry_policy=retry_policy)
Expand All @@ -250,6 +258,44 @@ def get_appconfig_data_client(cmd, name, connection_string, auth_mode, endpoint)
return azconfig_client


def get_appconfig_feature_flag_client(cmd, name, connection_string, auth_mode, endpoint):
feature_flag_client = None
retry_policy = _get_data_plane_retry_policy()

if auth_mode == "anonymous":
try:
feature_flag_client = FeatureFlagClient(
base_url=endpoint,
credential=AzureKeyCredential(key=""),
id_credential="",
user_agent=HttpHeaders.USER_AGENT,
transport=AuthHeaderRequestsTransport(),
retry_policy=retry_policy)
except (ValueError, TypeError) as ex:
raise CLIError("Failed to initialize FeatureFlagClient due to an exception: {}".format(str(ex)))

if auth_mode == "key":
connection_string = resolve_connection_string(cmd, name, connection_string)
try:
feature_flag_client = FeatureFlagClient.from_connection_string(connection_string=connection_string,
user_agent=HttpHeaders.USER_AGENT,
retry_policy=retry_policy)
except ValueError as ex:
raise CLIError("Failed to initialize FeatureFlagClient due to an exception: {}".format(str(ex)))

if auth_mode == "login":
credential, endpoint = resolve_login_credential(cmd, name, endpoint)
try:
feature_flag_client = FeatureFlagClient(credential=credential,
base_url=endpoint,
user_agent=HttpHeaders.USER_AGENT,
retry_policy=retry_policy)
except (ValueError, TypeError) as ex:
raise CLIError("Failed to initialize FeatureFlagClient due to an exception: {}".format(str(ex)))

return feature_flag_client


def is_json_content_type(content_type):
if not content_type:
return False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ def validate_feature_query_fields(namespace):
namespace.fields = fields


def validate_enhanced_feature_query_fields(namespace):
if namespace.fields:
namespace.fields = [field.lower() for field in namespace.fields]


def validate_snapshot_query_fields(namespace):
if namespace.fields:
fields = []
Expand Down
Loading
Loading