-
Notifications
You must be signed in to change notification settings - Fork 0
FeatureManagement CLI Part 1 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8dcfa36
6e07f35
abc0352
d521e67
b27d8f5
2acc8de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from enum import Enum | ||
| import json | ||
| import uuid | ||
| import azure.cli.command_modules.appconfig._azconfig.constants as constants | ||
|
|
||
|
|
@@ -50,7 +51,7 @@ def __str__(self): | |
| "\netag: " + self.etag + \ | ||
| "\nLast Modified: " + self.last_modified + \ | ||
| "\nContent Type: " + self.content_type + \ | ||
| "\nTags: " + '{!s}'.format(self.tags) | ||
| "\nTags: " + (str(self.tags) if self.tags else '') | ||
|
|
||
|
|
||
| class QueryFields(Enum): | ||
|
|
@@ -79,6 +80,8 @@ class QueryKeyValueOptions(object): | |
| A request ID that, if provided, can be used to help track the operation. | ||
| :ivar string correlation_request_id: | ||
| An ID that can be used to correlate the request with a more general operation. | ||
| :ivar string content_type: | ||
| Content_type of the key-value entry | ||
| ''' | ||
|
|
||
| empty_label = u'\0' | ||
|
|
@@ -88,14 +91,16 @@ def __init__(self, | |
| query_datetime=None, | ||
| fields=None, | ||
| client_request_id=None, | ||
| correlation_request_id=None): | ||
| correlation_request_id=None, | ||
| content_type=None): | ||
| self.label = label | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we support query by content_type. Currently we can only query kv by key/label/fields/datetime #Resolved
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| self.query_datetime = query_datetime | ||
| self.fields = fields | ||
| self.client_request_id = str( | ||
| uuid.uuid4()) if client_request_id is None else client_request_id | ||
| self.correlation_request_id = str( | ||
| uuid.uuid4()) if correlation_request_id is None else correlation_request_id | ||
| self.content_type = content_type | ||
|
|
||
|
|
||
| class QueryKeyValueCollectionOptions(object): | ||
|
|
@@ -114,6 +119,8 @@ class QueryKeyValueCollectionOptions(object): | |
| A request ID that, if provided, can be used to help track the operation. | ||
| :ivar string correlation_request_id: | ||
| An ID that can be used to correlate the request with a more general operation. | ||
| :ivar string content_type: | ||
| Content_type of the key-value entry | ||
| ''' | ||
|
|
||
| any_key = '*' | ||
|
|
@@ -126,7 +133,8 @@ def __init__(self, | |
| query_datetime=None, | ||
| fields=None, | ||
| client_request_id=None, | ||
| correlation_request_id=None): | ||
| correlation_request_id=None, | ||
| content_type=None): | ||
| self.key_filter = key_filter | ||
| self.label_filter = label_filter | ||
| self.query_datetime = query_datetime | ||
|
|
@@ -135,6 +143,7 @@ def __init__(self, | |
| uuid.uuid4()) if client_request_id is None else client_request_id | ||
| self.correlation_request_id = str( | ||
| uuid.uuid4()) if correlation_request_id is None else correlation_request_id | ||
| self.content_type = content_type | ||
|
|
||
|
|
||
| class ModifyKeyValueOptions(object): | ||
|
|
@@ -170,4 +179,4 @@ def __init__(self, user_agent=None, max_retries=None, max_retry_wait_time=None): | |
| self.user_agent = "AzconfigClient/{0}/CLI".format( | ||
| constants.Versions.SDKVersion) if user_agent is None else user_agent | ||
| self.max_retries = 9 if max_retries is None else max_retries | ||
| self.max_retry_wait_time = 30 if max_retry_wait_time is None else max_retry_wait_time | ||
| self.max_retry_wait_time = 30 if max_retry_wait_time is None else max_retry_wait_time | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| from enum import Enum | ||
| import json | ||
|
|
||
| # pylint: disable=too-few-public-methods | ||
| # pylint: disable=too-many-instance-attributes | ||
|
|
||
| class FeatureState(Enum): | ||
| OFF = 1 | ||
| ON = 2 | ||
| CONDITIONAL = 3 | ||
|
|
||
|
|
||
| class FeatureQueryFields(Enum): | ||
| KEY = 0x001 | ||
| LABEL = 0x002 | ||
| LAST_MODIFIED = 0x020 | ||
| LOCKED = 0x040 | ||
| STATE = 0x100 | ||
| DESCRIPTION = 0x200 | ||
| CONDITIONS = 0x400 | ||
| ALL = KEY | LABEL | LAST_MODIFIED | LOCKED | STATE | DESCRIPTION | CONDITIONS | ||
|
|
||
|
|
||
| class FeatureFlagDisplay(object): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
rename it to just FeatureFlag, along with several helper method, variable name. No need to call it display |
||
| ''' | ||
| Feature Flag schema as displayed to the user. | ||
|
|
||
| :ivar str key: | ||
| FeatureName (key) of the entry. | ||
| :ivar str label: | ||
| Label of the entry. | ||
| :ivar str state: | ||
| Represents if the Feature flag is On/Off/Conditionally On | ||
| :ivar str description: | ||
| Description of Feature Flag | ||
| :ivar bool locked: | ||
| Represents whether the feature flag is locked. | ||
| :ivar datetime last_modified: | ||
| A datetime object representing the last time the feature flag was modified. | ||
| :ivar str etag: | ||
| The ETag contains a value that you can use to perform operations. | ||
| :ivar dict {string, FeatureFilter[]>} conditions: | ||
| Dictionary that contains client_filters List (and server_filters List in future) | ||
| ''' | ||
|
|
||
| def __init__(self, | ||
| key, | ||
| label=None, | ||
| state=None, | ||
| description=None, | ||
| conditions=None): | ||
| self.key = key | ||
| self.label = label | ||
| self.state = state.name.lower() | ||
| self.description = description | ||
| self.conditions = conditions | ||
| self.last_modified = None | ||
| self.locked = None | ||
|
|
||
| def __str__(self): | ||
| featureflagdisplay = { | ||
| "Key": self.key, | ||
| "Label": self.label, | ||
| "State": self.state, | ||
| "Locked": self.locked, | ||
| "Description": self.description, | ||
| "Last Modified": self.last_modified, | ||
| "Conditions": custom_serialize_conditions(self.conditions) | ||
| } | ||
|
|
||
| return json.dumps(featureflagdisplay, indent=2) | ||
|
|
||
|
|
||
| class FeatureFlagValue(object): | ||
| ''' | ||
| Schema of Value inside KeyValue when key is a Feature Flag. | ||
|
|
||
| :ivar str id: | ||
| ID (key) of the feature. | ||
| :ivar str description: | ||
| Description of Feature Flag | ||
| :ivar bool enabled: | ||
| Represents if the Feature flag is On/Off/Conditionally On | ||
| :ivar str label: | ||
| Label of the entry. | ||
| :ivar dict {string, FeatureFilter[]>} conditions: | ||
| Disctionary that contains client_filters List (and server_filters List in future) | ||
| ''' | ||
| def __init__(self, | ||
| id, | ||
| description=None, | ||
| enabled=None, | ||
| label=None, | ||
| conditions=None): | ||
| self.id = id | ||
| self.description = description | ||
| self.enabled = enabled | ||
| self.label = label | ||
| self.conditions = conditions | ||
|
|
||
| def __str__(self): | ||
| featureflagvalue = { | ||
| "id": self.id, | ||
| "description": self.description, | ||
| "enabled": self.enabled, | ||
| "label": self.label, | ||
| "conditions": custom_serialize_conditions(self.conditions) | ||
| } | ||
|
|
||
| return json.dumps(featureflagvalue, indent=2) | ||
|
|
||
|
|
||
| class FeatureFilter(object): | ||
| ''' | ||
| Feature filters class. | ||
|
|
||
| :ivar str Name: | ||
| Name of the filter | ||
| :ivar dict {str, str} parameters: | ||
| Name-Value pairs of parameters | ||
| ''' | ||
|
|
||
| def __init__(self, | ||
| name, | ||
| parameters=None): | ||
| self.name = name | ||
| self.parameters = parameters | ||
|
|
||
| def __repr__(self): | ||
| featurefilter = { | ||
| "name": self.name, | ||
| "parameters": self.parameters | ||
| } | ||
| return json.dumps(featurefilter,indent=2) | ||
|
|
||
|
|
||
| # Helper Function to serialize Conditions | ||
| # Conditions will be dict {str, List[FeatureFilter]} | ||
| def custom_serialize_conditions(object): | ||
| featurefilterdict = {} | ||
| if object: | ||
| for key,value in object.items(): | ||
| featurefilters = [] | ||
| for filter in value: | ||
| featurefilters.append(str(filter)) | ||
| featurefilterdict[key] = featurefilters | ||
| return featurefilterdict | ||
|
|
||
|
|
||
| def map_keyvalue_to_featureflagdisplay(keyvalue, show_conditions=True): | ||
| feature_flag_value = map_json_to_featureflagvalue(json.loads(keyvalue.value)) | ||
| feature_flag_display = map_featureflag_value_to_display(feature_flag_value) | ||
| feature_flag_display.locked = keyvalue.locked | ||
| feature_flag_display.last_modified = keyvalue.last_modified | ||
|
|
||
| # By Default, we will try to show conditions unless the user has | ||
| # specifically filtered them using --fields arg. | ||
| # But in some operations like 'Delete feature', we don't want | ||
| # to display all the conditions as a result of delete operation | ||
| if not show_conditions: | ||
| del feature_flag_display.conditions | ||
| return feature_flag_display | ||
|
|
||
|
|
||
| def map_featureflag_value_to_display(featureflagvalue): | ||
| state = FeatureState.OFF | ||
| if (getattr(featureflagvalue, 'enabled')): | ||
| state = FeatureState.ON | ||
|
|
||
| conditions = getattr(featureflagvalue, 'conditions') | ||
|
|
||
| # if conditions["client_filters"] list is not empty, make state conditional | ||
| # generalizing for conditions["server_filters"] in future | ||
| for value in conditions.values(): | ||
| if value and state == FeatureState.ON: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for the for loop since this is a dict. As conditions can contains any keys not only client_filters, server_filters, we should explicitly check. filters = conditions.get("client_filter", []) |
||
| state = FeatureState.CONDITIONAL | ||
| break | ||
|
|
||
| featureflag = FeatureFlagDisplay( | ||
| getattr(featureflagvalue, 'id'), | ||
| getattr(featureflagvalue, 'label'), | ||
| state, | ||
| getattr(featureflagvalue, 'description'), | ||
| conditions) | ||
|
|
||
| return featureflag | ||
|
|
||
|
|
||
| def map_json_to_featureflagvalue(json_object): | ||
| conditions = __get_value(json_object, 'conditions') | ||
| conditions_with_filters = {} | ||
| conditions_with_filters = custom_serialize_conditions(conditions) | ||
|
|
||
| featureflagvalue = FeatureFlagValue( | ||
| __get_value(json_object, 'id'), | ||
| __get_value(json_object, 'description'), | ||
| __get_value(json_object, 'enabled'), | ||
| __get_value(json_object, 'label'), | ||
| conditions_with_filters) | ||
| return featureflagvalue | ||
|
|
||
|
|
||
| def map_json_to_featurefilter(json_object): | ||
| featurefilters = FeatureFilter( | ||
| __get_value(json_object, 'name'), | ||
| __get_value(json_object, 'parameters')) | ||
| return featurefilters | ||
|
|
||
|
|
||
| def __get_value(item, argument): | ||
| try: | ||
| return item[argument] | ||
| except (KeyError, TypeError, IndexError): | ||
| return None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -219,3 +219,27 @@ | |
| text: | ||
| az appconfig kv unlock --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --key color --label test --yes | ||
| """ | ||
|
|
||
| helps['appconfig feature show'] = """ | ||
| type: command | ||
| short-summary: Show all attributes of a feature flag. | ||
| examples: | ||
| - name: Show a feature flag using App Configuration name with a specific label | ||
| text: | ||
| az appconfig feature show -n MyAppConfiguration --feature color --label MyLabel | ||
| - name: Show a feature flag using connection string and field filters | ||
| text: | ||
| az appconfig feature show --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --fields key locked conditions state | ||
| """ | ||
|
|
||
| helps['appconfig feature set'] = """ | ||
| type: command | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add help for feature lock/unlock/enable/disable #Resolved |
||
| short-summary: Set a feature flag. | ||
| examples: | ||
| - name: Set a feature flag with label MyLabel. | ||
| text: | ||
| az appconfig feature set -n MyAppConfiguration --feature color --label MyLabel | ||
| - name: Set a feature flag with null label using connection string and set a description. | ||
| text: | ||
| az appconfig feature set --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --feature color --description "This is a colorful feature" | ||
| """ | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can revert changes in this module. #Resolved