Skip to content
Merged
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
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 Expand Up @@ -170,4 +170,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
258 changes: 258 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,258 @@
# --------------------------------------------------------------------------------------------
# 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.util import CLIError
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/"
DEFAULT_CONDITIONS = {'client_filters':[]}

# 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 FeatureFlagDisplay(object):
Copy link

@shenmuxiaosen shenmuxiaosen Sep 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeatureFlagDisplay [](start = 6, length = 18)

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,
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 __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 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 Exceptions #

class InvalidJsonException(ValueError):
def __init__(self, message):
self.message = message
super(InvalidJsonException, self).__init__(message)


class UnsupportedValuesException(ValueError):
def __init__(self, message):
self.message = message
super(UnsupportedValuesException, self).__init__(message)



# Feature Flag Helper Functions #

def custom_serialize_conditions(conditions_dict):
'''
Helper Function to serialize Conditions

Input: conditions_dict
Dictionary of {str, List[FeatureFilter]}

Return: JSON serializable Dictionary
'''
featurefilterdict = {}
if conditions_dict:
for key,value in conditions_dict.items():
featurefilters = []
for filter in value:
featurefilters.append(str(filter))
featurefilterdict[key] = featurefilters
return featurefilterdict


def map_keyvalue_to_featureflagdisplay(keyvalue, show_conditions=True):
'''
Helper Function to convert KeyValue object to FeatureFlagDisplay object

Input: keyvalue
KeyValue object to be converted

Input: show_conditions
Boolean for controlling whether we want to display "Conditions" or not

Return: FeatureFlagDisplay object
'''
key = getattr(keyvalue, 'key')
feature_name = key[len(FEATURE_FLAG_PREFIX):]

# we check that value retrieved is a valid json and only has the fields supported by backend.
# if it's invalid, we throw exception
# For all other exceptions, we let the outer try/except handle it.
try:
feature_flag_value = map_valuestr_to_valuedict(keyvalue)
except (UnsupportedValuesException, InvalidJsonException) as exception:
raise ValueError(f"Invalid Value found for Key '{key}'. Aborting operation\n" + str(exception))

state = FeatureState.OFF
if feature_flag_value.get('enabled', False):
state = FeatureState.ON

conditions = feature_flag_value.get('conditions', DEFAULT_CONDITIONS)

# if conditions["client_filters"] list is not empty, make state conditional
filters = conditions.get("client_filters", [])
if filters and state == FeatureState.ON:
state = FeatureState.CONDITIONAL

feature_flag_display = FeatureFlagDisplay(feature_name,
getattr(keyvalue, 'label', ""),
Copy link

@shenmuxiaosen shenmuxiaosen Sep 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate feature flag schema and error out if mismatch. So when we deserialize conditions from value, it must contains fields like id, enabled, conditions.... #Resolved

state,
feature_flag_value.get('description', ""),
conditions,
getattr(keyvalue, 'locked', False),
getattr(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_valuestr_to_valuedict(keyvalue):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map_valuestr_to_valuedict [](start = 4, length = 25)

it doesn't make sense to pass in a keyvalue object but the method name is map valuestr to dict. We should just pass kv.value in. This helper method should have no knowledge about the feature flag name. It can still throw exceptions.

'''
Helper Function to convert value string to a VALID value dictionary.
Throws Exception if value is invalid.

Input: keyvalue
KeyValue object to be converted

Return: Valid value dictionary

Raises:
UnsupportedValuesException: raised when feature flag value is missing required fields or contains other invalid fields
InvalidJsonException: raised when JSON decode error is thrown because value string cannot be deserialized to a valid JSON
'''

feature_flag_value = {}
key = getattr(keyvalue, 'key')
feature_name = key[len(FEATURE_FLAG_PREFIX):]

valuestr = getattr(keyvalue, 'value', "")
if valuestr:
try:
# Make sure value string is a valid json
feature_flag_value = json.loads(valuestr)

# Make sure value json has all the fields we support in the backend
valid_fields = {'id', 'description', 'enabled', 'label', 'conditions'}
if valid_fields != feature_flag_value.keys():
error_msg = f"This feature flag cannot be processed because it is missing required values or it contains unsupported values.\n"
Copy link

@shenmuxiaosen shenmuxiaosen Sep 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feature_flag_value is a dict and doesn't preserve order. Instead of directly compare two lists here, we can do
for field in fields:
if field not in feature_flag_value:
error out the specific missing field #Resolved

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are comparing two sets, so order doesn't matter. Also, feature_flag_value dict is guaranteed to have only unique keys. So this comparison should work.


In reply to: 326753124 [](ancestors = 326753124)

raise UnsupportedValuesException(f"Feature flag {feature_name} contains invalid value. " + error_msg)

except UnsupportedValuesException as exception:
raise UnsupportedValuesException(str(exception))

except ValueError as exception:
error_msg = f"Unable to decode the following JSON value: \n{valuestr}. \nFull Exception: \n{str(exception)}"
raise InvalidJsonException(f"Feature flag {feature_name} contains invalid value. " + error_msg)

Copy link

@shenmuxiaosen shenmuxiaosen Sep 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider log error message here instead of put it in InvalidJsonException message. Since the caller will catch these exceptions and create it own error message. #Resolved

except Exception as exception:
error_msg = f"Exception while parsing value for feature: {feature_name}\nValue: {valuestr}\n"
raise Exception(error_msg + str(exception))

return feature_flag_value


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

13 changes: 13 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 @@ -17,6 +17,8 @@ def configstore_credential_format(result):
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 _output_format(result, format_group):
if 'value' in result and isinstance(result['value'], list):
Expand Down Expand Up @@ -59,6 +61,17 @@ 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 _format_datetime(date_string):
from dateutil.parser import parse
try:
Expand Down
Loading