Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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

@shenmuxiaosen shenmuxiaosen Sep 19, 2019

Copy link
Copy Markdown

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



Expand All @@ -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

@shenmuxiaosen shenmuxiaosen Sep 17, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Expand Up @@ -4,6 +4,7 @@
# --------------------------------------------------------------------------------------------

from enum import Enum
import json
import uuid
import azure.cli.command_modules.appconfig._azconfig.constants as constants

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'
Expand All @@ -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

@shenmuxiaosen shenmuxiaosen Sep 18, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed


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

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):
Expand All @@ -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 = '*'
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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

@shenmuxiaosen shenmuxiaosen Sep 17, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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,

@shenmuxiaosen shenmuxiaosen Sep 17, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Key [](start = 13, length = 3)

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
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
28 changes: 26 additions & 2 deletions src/azure-cli/azure/cli/command_modules/appconfig/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

@shenmuxiaosen shenmuxiaosen Sep 17, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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

@shenmuxiaosen shenmuxiaosen Sep 20, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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"
"""
18 changes: 17 additions & 1 deletion src/azure-cli/azure/cli/command_modules/appconfig/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, _):
Expand All @@ -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'])

@shenmuxiaosen shenmuxiaosen Sep 17, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

get_enum_type [](start = 17, length = 13)

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.'
Expand Down Expand Up @@ -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')

@shenmuxiaosen shenmuxiaosen Sep 20, 2019

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.')
Loading