Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bf00b70
FeatureManagement CLI Part 1 (#1)
avanigupta Sep 20, 2019
8c78d50
Merge branch 'dev' of https://github.com/avanigupta/azure-cli into dev
avanigupta Sep 20, 2019
61ef19e
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta Sep 25, 2019
9fcb3fd
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta Sep 27, 2019
9889cd3
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta Oct 8, 2019
f2024c6
Final changes for feature management CLI (#2)
avanigupta Oct 8, 2019
f5c45a5
Fix styling issues
avanigupta Oct 8, 2019
d7cc069
String formatting for python 2.7 compatibility
avanigupta Oct 8, 2019
5026d7d
Python 2.7 compatibility for unit tests
avanigupta Oct 8, 2019
d8a5c39
fix id attribute for feature flag value
avanigupta Oct 8, 2019
c6d2881
Modify list feature default label behavior
avanigupta Oct 9, 2019
30aace3
Check content type for features
avanigupta Oct 9, 2019
7bb402b
CLI team's suggested changes
avanigupta Oct 15, 2019
c22469f
Merge remote-tracking branch 'upstream/dev' into dev
avanigupta Oct 15, 2019
abc3f20
Update test recordings
avanigupta Oct 15, 2019
e41bf4a
Merging with latest code
avanigupta Oct 16, 2019
84a58c7
Changing commands to custom_commands
avanigupta Oct 16, 2019
83a8ff2
resolving conflict
avanigupta Oct 17, 2019
2902c69
changed empty label behavior for delete feature
avanigupta Oct 17, 2019
17b8f88
Change warning message
avanigupta Oct 17, 2019
f86293d
Removing label from FeatureFlagValue
avanigupta Oct 18, 2019
414e8e0
resolve conflicts
avanigupta Oct 21, 2019
f4fce0e
Use shell_safe_json_parse for deserialiization
avanigupta Oct 21, 2019
b655295
Merge branch 'dev' into dev
avanigupta Oct 22, 2019
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 @@ -13,6 +13,7 @@ Release History

**AppConfig**

* Add appconfig feature command group to manage feature flags stored in an App Configuration.
* Minor bug fix for appconfig kv export to file command. Stop reading dest file contents during export.

**AppService**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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):
Expand Down
276 changes: 276 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appconfig/_featuremodels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# --------------------------------------------------------------------------------------------
# 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
from azure.cli.core.util import shell_safe_json_parse

# 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 = shell_safe_json_parse(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
27 changes: 27 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 @@ -18,6 +18,14 @@ def keyvalue_entry_format(result):
return _output_format(result, _keyvalue_entry_format_group)


def featureflag_entry_format(result):
return _output_format(result, _featureflag_entry_format_group)


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


def _output_format(result, format_group):
if 'value' in result and isinstance(result['value'], list):
result = result['value']
Expand Down Expand Up @@ -59,6 +67,25 @@ def _keyvalue_entry_format_group(item):
])


def _featureflag_entry_format_group(item):
return OrderedDict([
('KEY', _get_value(item, 'key')),
('LABEL', _get_value(item, 'label')),
('STATE', _get_value(item, 'state')),
('LOCKED', _get_value(item, 'locked')),
('DESCRIPTION', _get_value(item, 'description')),
('LAST MODIFIED', _format_datetime(_get_value(item, 'lastModified'))),
('CONDITIONS', _get_value(item, 'conditions'))
])


def _featurefilter_entry_format_group(item):
return OrderedDict([
('NAME', _get_value(item, 'name')),
('PARAMETERS', _get_value(item, 'parameters'))
])


def _format_datetime(date_string):
from dateutil.parser import parse
try:
Expand Down
Loading