-
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 1 commit
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 |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| import json | ||
| import azure.cli.command_modules.appconfig._azconfig.models as models | ||
|
|
||
|
|
||
|
|
@@ -27,8 +28,54 @@ def map_json_to_keyvalues(json_objects): | |
| return keyvalue_list | ||
|
|
||
|
|
||
| def map_featureflag_value_to_display(featureflagvalue): | ||
| state = models.FeatureState.OFF | ||
|
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. We shouldn't add new models/mappers in data plane sdk. From sdk perspective, everything is key value. The concept of feature flag lives in a convenient layer. In our case, we should put these models in under /appconfig instead of /appconfig/_azconfig. #Resolved |
||
| if (getattr(featureflagvalue, 'enabled')): | ||
| state = models.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 is models.FeatureState.ON: | ||
| state = models.FeatureState.CONDITIONAL | ||
| break | ||
|
|
||
| featureflag = models.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 = models.custom_serialize_conditions(conditions) | ||
|
|
||
| featureflagvalue = models.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 = models.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 |
|---|---|---|
|
|
@@ -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): | ||
|
|
@@ -171,3 +180,145 @@ def __init__(self, user_agent=None, max_retries=None, max_retry_wait_time=None): | |
| 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 | ||
|
|
||
|
|
||
| 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): | ||
| ''' | ||
| 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 | ||
|
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. why we can't initialize last modified and locked in ctor #Resolved |
||
|
|
||
| def __str__(self): | ||
| featureflagdisplay = { | ||
| "Key": self.key, | ||
|
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.
Do we want to call it key or name? In the design doc, it is called name. I am fine to both. #Resolved |
||
| "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 = {} | ||
| for key,value in object.items(): | ||
| featurefilters = [] | ||
| for filter in value: | ||
| featurefilters.append(str(filter)) | ||
| featurefilterdict[key] = featurefilters | ||
| return featurefilterdict | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,9 +159,9 @@ | |
| - name: List a specfic key for any label start with v1. using connection string. | ||
| text: | ||
| az appconfig kv list --key color --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --label v1.* | ||
| - name: List all keys with any labels and query only key, value and tags. | ||
| - name: List all keys with any labels and query only key and value. | ||
| text: | ||
|
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. We can leave the example. There is a bug in _params.py, tags is not added as a valid field. #Resolved |
||
| az appconfig kv list --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --fields key value tags --datetime "2019-05-01T11:24:12Z" | ||
| az appconfig kv list --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --fields key value --datetime "2019-05-01T11:24:12Z" | ||
| - name: List 150 key-values with any labels. | ||
| text: | ||
| az appconfig kv list --connection-string Endpoint=https://contoso.azconfig.io;Id=xxx;Secret=xxx --top 150 | ||
|
|
@@ -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" | ||
| """ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ | |
| validate_connection_string, validate_datetime, | ||
| validate_export, validate_import, | ||
| validate_import_depth, validate_query_fields, | ||
| validate_separator) | ||
| validate_feature_query_fields, validate_separator) | ||
|
|
||
|
|
||
| def load_arguments(self, _): | ||
|
|
@@ -30,6 +30,12 @@ def load_arguments(self, _): | |
| validator=validate_query_fields, | ||
| arg_type=get_enum_type(['key', 'value', 'label', 'content_type', 'etag', 'locked', 'last_modified']) | ||
|
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 think we miss tags here. Would you mind to add it in your pr? #Resolved |
||
| ) | ||
| feature_fields_arg_type = CLIArgumentType( | ||
| nargs='+', | ||
| help='Customize output fields for Feature Flags.', | ||
| validator=validate_feature_query_fields, | ||
| arg_type=get_enum_type(['key', 'label', 'locked' ,'last_modified', 'state', 'description', 'conditions']) | ||
| ) | ||
| datatime_filter_arg_type = CLIArgumentType( | ||
| validator=validate_datetime, | ||
| help='Format: "YYYY-MM-DDThh:mm:ssZ". If no time zone specified, use UTC by default.' | ||
|
|
@@ -142,3 +148,13 @@ def load_arguments(self, _): | |
| c.argument('name', id_part=None) | ||
| c.argument('key', help='If no key specified, return all keys by default. Support star sign as filters, for instance abc* means keys with abc as prefix. Similarly, *abc and *abc* are also supported.') | ||
| c.argument('label', help="If no label specified, list all labels. Support star sign as filters, for instance abc* means labels with abc as prefix. Similarly, *abc and *abc* are also supported.") | ||
|
|
||
| with self.argument_context('appconfig feature show') as c: | ||
| c.argument('feature', help='Name of the Feature flag to be retrieved') | ||
|
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 argument context for lock/unlock/enable/disable #Resolved |
||
| c.argument('label', help="If no label specified, show entry with null label. Does NOT support filters like other commands.") | ||
| c.argument('fields', arg_type=feature_fields_arg_type) | ||
|
|
||
| with self.argument_context('appconfig feature set') as c: | ||
| c.argument('feature', help='Name of the Feature flag to be set.') | ||
| c.argument('label', help="If no label specified, set the feature flag with null label by default") | ||
| c.argument('description', help='Description of the feature flag to be set.') | ||
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