-
Notifications
You must be signed in to change notification settings - Fork 3.5k
AppConfig: Support CRUD of feature flags #10774
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
Merged
Merged
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
bf00b70
FeatureManagement CLI Part 1 (#1)
avanigupta 8c78d50
Merge branch 'dev' of https://github.com/avanigupta/azure-cli into dev
avanigupta 61ef19e
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta 9fcb3fd
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta 9889cd3
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta f2024c6
Final changes for feature management CLI (#2)
avanigupta f5c45a5
Fix styling issues
avanigupta d7cc069
String formatting for python 2.7 compatibility
avanigupta 5026d7d
Python 2.7 compatibility for unit tests
avanigupta d8a5c39
fix id attribute for feature flag value
avanigupta c6d2881
Modify list feature default label behavior
avanigupta 30aace3
Check content type for features
avanigupta 7bb402b
CLI team's suggested changes
avanigupta c22469f
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta abc3f20
Update test recordings
avanigupta e41bf4a
Merging with latest code
avanigupta 84a58c7
Changing commands to custom_commands
avanigupta 83a8ff2
resolving conflict
avanigupta 2902c69
changed empty label behavior for delete feature
avanigupta 17b8f88
Change warning message
avanigupta f86293d
Removing label from FeatureFlagValue
avanigupta 414e8e0
resolve conflicts
avanigupta f4fce0e
Use shell_safe_json_parse for deserialiization
avanigupta b655295
Merge branch 'dev' into dev
avanigupta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
275 changes: 275 additions & 0 deletions
275
src/azure-cli/azure/cli/command_modules/appconfig/_featuremodels.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # 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 | ||
| from knack.log import get_logger | ||
|
|
||
| # pylint: disable=too-few-public-methods | ||
| # pylint: disable=too-many-instance-attributes | ||
|
|
||
| logger = get_logger(__name__) | ||
| FEATURE_FLAG_PREFIX = ".appconfig.featureflag/" | ||
|
|
||
| # Feature Flag Models # | ||
|
|
||
|
|
||
| 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 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 dict {string, FeatureFilter[]} conditions: | ||
| Dictionary that contains client_filters List (and server_filters List in future) | ||
| ''' | ||
|
|
||
| def __init__(self, | ||
| id_, | ||
| description=None, | ||
| enabled=None, | ||
| conditions=None): | ||
| self.id = id_ | ||
| self.description = description | ||
| self.enabled = enabled | ||
| self.conditions = conditions | ||
|
|
||
| def __repr__(self): | ||
| featureflagvalue = { | ||
| "id": self.id, | ||
| "description": self.description, | ||
| "enabled": self.enabled, | ||
| "conditions": custom_serialize_conditions(self.conditions) | ||
| } | ||
|
|
||
| return json.dumps(featureflagvalue, indent=2) | ||
|
|
||
|
|
||
| class FeatureFlag(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, | ||
| locked=None, | ||
| last_modified=None): | ||
| self.key = key | ||
| self.label = label | ||
| self.state = state.name.lower() | ||
| self.description = description | ||
| self.conditions = conditions | ||
| self.last_modified = last_modified | ||
| self.locked = locked | ||
|
|
||
| def __repr__(self): | ||
| featureflag = { | ||
| "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(featureflag, 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) | ||
|
|
||
| # Feature Flag Helper Functions # | ||
|
|
||
|
|
||
| def custom_serialize_conditions(conditions_dict): | ||
| ''' | ||
| Helper Function to serialize Conditions | ||
|
|
||
| Args: | ||
| conditions_dict - Dictionary of {str, List[FeatureFilter]} | ||
|
|
||
| Return: | ||
| JSON serializable Dictionary | ||
| ''' | ||
| featurefilterdict = {} | ||
|
|
||
| for key, value in conditions_dict.items(): | ||
| featurefilters = [] | ||
| for featurefilter in value: | ||
| featurefilters.append(str(featurefilter)) | ||
| featurefilterdict[key] = featurefilters | ||
| return featurefilterdict | ||
|
|
||
|
|
||
| def map_keyvalue_to_featureflag(keyvalue, show_conditions=True): | ||
| ''' | ||
| Helper Function to convert KeyValue object to FeatureFlag object for display | ||
|
|
||
| Args: | ||
| keyvalue - KeyValue object to be converted | ||
| show_conditions - Boolean for controlling whether we want to display "Conditions" or not | ||
|
|
||
| Return: | ||
| FeatureFlag object | ||
| ''' | ||
| feature_name = keyvalue.key[len(FEATURE_FLAG_PREFIX):] | ||
|
|
||
| feature_flag_value = map_keyvalue_to_featureflagvalue(keyvalue) | ||
|
|
||
| state = FeatureState.OFF | ||
| if feature_flag_value.enabled: | ||
| state = FeatureState.ON | ||
|
|
||
| conditions = feature_flag_value.conditions | ||
|
|
||
| # if conditions["client_filters"] list is not empty, make state conditional | ||
| filters = conditions["client_filters"] | ||
|
|
||
| if filters and state == FeatureState.ON: | ||
| state = FeatureState.CONDITIONAL | ||
|
|
||
| feature_flag = FeatureFlag(feature_name, | ||
| keyvalue.label, | ||
| state, | ||
| feature_flag_value.description, | ||
| conditions, | ||
| keyvalue.locked, | ||
| 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.conditions | ||
| return feature_flag | ||
|
|
||
|
|
||
| def map_keyvalue_to_featureflagvalue(keyvalue): | ||
| ''' | ||
| Helper Function to convert value string to a valid FeatureFlagValue. | ||
| Throws Exception if value is an invalid JSON. | ||
|
|
||
| Args: | ||
| keyvalue - KeyValue object | ||
|
|
||
| Return: | ||
| Valid FeatureFlagValue object | ||
| ''' | ||
|
|
||
| default_conditions = {'client_filters': []} | ||
|
|
||
| try: | ||
| # Make sure value string is a valid json | ||
| feature_flag_dict = json.loads(keyvalue.value) | ||
| feature_name = keyvalue.key[len(FEATURE_FLAG_PREFIX):] | ||
|
|
||
| # Make sure value json has all the fields we support in the backend | ||
| valid_fields = { | ||
| 'id', | ||
| 'description', | ||
| 'enabled', | ||
| 'conditions'} | ||
| if valid_fields != feature_flag_dict.keys(): | ||
| logger.debug("'%s' feature flag is missing required values or it contains ", feature_name + | ||
| "unsupported values. Setting missing value to defaults and ignoring unsupported values\n") | ||
|
|
||
| conditions = feature_flag_dict.get('conditions', default_conditions) | ||
| client_filters = conditions.get('client_filters', []) | ||
|
|
||
| # Convert all filters to FeatureFilter objects | ||
| client_filters_list = [] | ||
| for client_filter in client_filters: | ||
| # If there is a filter, it should always have a name | ||
| # In case it doesn't, ignore this filter | ||
| name = client_filter.get('name') | ||
| if name: | ||
| params = client_filter.get('parameters', {}) | ||
| client_filters_list.append(FeatureFilter(name, params)) | ||
| else: | ||
| logger.warning("Ignoring this filter without the 'name' attribute:\n%s", | ||
| json.dumps(client_filter, indent=2)) | ||
| conditions['client_filters'] = client_filters_list | ||
|
|
||
| feature_flag_value = FeatureFlagValue(id_=feature_name, | ||
| description=feature_flag_dict.get( | ||
| 'description', ''), | ||
| enabled=feature_flag_dict.get( | ||
| 'enabled', False), | ||
| conditions=conditions) | ||
|
|
||
| except ValueError as exception: | ||
| error_msg = "Invalid value. Unable to decode the following JSON value: \n" +\ | ||
| "{0}\nFull exception: \n{1}".format(keyvalue.value, str(exception)) | ||
| raise ValueError(error_msg) | ||
|
|
||
| except: | ||
| logger.debug("Exception while parsing value:\n%s\n", keyvalue.value) | ||
| raise | ||
|
|
||
| return feature_flag_value | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.