From 640a1fee6e08e25e5e82b9db296052edea74ca7a Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Fri, 17 Nov 2023 12:17:56 +0800 Subject: [PATCH 1/9] Add diff function for selector --- .../model/configuration/_selector_index.py | 116 ++++++++++++++++++ .../configuration/_subresource_selector.py | 23 ++++ 2 files changed, 139 insertions(+) diff --git a/src/aaz_dev/command/model/configuration/_selector_index.py b/src/aaz_dev/command/model/configuration/_selector_index.py index d1a44b61..bf11c67e 100644 --- a/src/aaz_dev/command/model/configuration/_selector_index.py +++ b/src/aaz_dev/command/model/configuration/_selector_index.py @@ -9,6 +9,7 @@ from ._schema import CMDSchemaField from ._arg_builder import CMDArgBuilder +from ._utils import CMDDiffLevelEnum class CMDSelectorIndexBase(Model): @@ -45,6 +46,16 @@ def _claim_polymorphic(cls, data): return data.TYPE_VALUE == cls.TYPE_VALUE return False + def _diff_base(self, old, level, diff): + return diff + + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + diff = self._diff_base(old, level, diff) + return diff + class CMDSelectorIndexBaseField(PolyModelType): @@ -100,6 +111,20 @@ def _claim_polymorphic(cls, data): return isinstance(data, CMDSelectorIndex) return False + def _diff(self, old, level, diff): + if level >= CMDDiffLevelEnum.BreakingChange: + if self.name != old.name: + diff["name"] = f"{old.name} != {self.name}" + return diff + + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + diff = self._diff_base(old, level, diff) + diff = self._diff(old, level, diff) + return diff + class CMDSelectorIndexField(PolyModelType): @@ -140,6 +165,27 @@ def generate_args(self, ref_args, var_prefix): return args + def diff(self, old, level): + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if self.property != old.property: + diff["property"] = f"{old.property} != {self.property}" + if self.value != old.value: + diff["value"] = f"{old.value} != {self.value}" + + if (not self.prop) != (not old.prop): + diff["prop"] = f"New prop" if self.prop else f"Miss prop" + elif self.prop: + if prop_diff := self.prop.diff(old.prop, level): + diff["prop"] = prop_diff + + if (not self.discriminator) != (not old.discriminator): + diff["discriminator"] = f"New discriminator" if self.discriminator else f"Miss discriminator" + elif self.discriminator: + if disc_diff := self.discriminator.diff(old.discriminator, level): + diff["discriminator"] = disc_diff + return diff + def reformat(self, **kwargs): if self.prop: self.prop.reformat(**kwargs) @@ -166,6 +212,20 @@ def generate_args(self, ref_args, var_prefix): var_prefix += '{}' args.extend(self.item.generate_args(ref_args, var_prefix)) return args + + def diff(self, old, level): + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if diff_item := self.item.diff(old.item): + diff['item'] = diff_item + identifiers_diff = _diff_identifiers( + self.identifiers or [], + old.identifiers or [], + level + ) + if identifiers_diff: + diff["identifiers"] = identifiers_diff + return diff def reformat(self, **kwargs): if self.item: @@ -208,6 +268,27 @@ def reformat(self, **kwargs): if self.additional_props: self.additional_props.reformat(**kwargs) + def _diff_base(self, old, level, diff): + if level >= CMDDiffLevelEnum.BreakingChange: + if (not self.prop) != (not old.prop): + diff['prop'] = f"New prop" if self.prop else f"Miss prop" + elif self.prop: + if prop_diff := self.prop.diff(old.prop, level): + diff["prop"] = prop_diff + + if (not self.discriminator) != (not old.discriminator): + diff['discriminator'] = f"New discriminator" if self.discriminator else f"Miss discriminator" + elif self.discriminator: + if discriminator_diff := self.discriminator.diff(old.discriminator, level): + diff["discriminator"] = discriminator_diff + + if (not self.additional_props) != (not old.additional_props): + diff['additional_props'] = f"New additional_props" if self.additional_props else f"Miss additional_props" + elif self.additional_props: + if additional_props_diff := self.additional_props.diff(old.additional_props, level): + diff["additional_props"] = additional_props_diff + return diff + class CMDObjectIndex(CMDObjectIndexBase, CMDSelectorIndex): @@ -253,6 +334,22 @@ def reformat(self, **kwargs): for identifier in self.identifiers: identifier.reformat(**kwargs) self.identifiers = sorted(self.identifiers, key=lambda i: i.name) + + def _diff_base(self, old, level, diff): + if level >= CMDDiffLevelEnum.BreakingChange: + if (not self.item) != (not old.item): + diff['item'] = f"New item" if self.item else f"Miss item" + elif self.item: + if item_diff := self.item.diff(old.item, level): + diff["item"] = item_diff + identifiers_diff = _diff_identifiers( + self.identifiers or [], + old.identifiers or [], + level + ) + if identifiers_diff: + diff["identifiers"] = identifiers_diff + return diff class CMDArrayIndex(CMDArrayIndexBase, CMDSelectorIndex): @@ -264,3 +361,22 @@ def generate_args(self, ref_args, var_prefix): var_prefix += f'.{self.name}' return self._generate_args_base(ref_args, var_prefix) + + +def _diff_identifiers(self_identifiers, old_identifiers, level): + identifiers_diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + identifiers_dict = {identifier.name: identifier for identifier in self_identifiers} + for old_identifier in old_identifiers: + if old_identifier.name not in identifiers_dict: + identifiers_diff[old_identifier.name] = "Miss identifier" + else: + identifier = identifiers_dict.pop(old_identifier.name) + diff = identifier.diff(old_identifier, level) + if diff: + identifiers_diff[old_identifier.name] = diff + + for identifier in identifiers_dict.values(): + identifiers_diff[identifier.name] = "New identifier" + + return identifiers_diff diff --git a/src/aaz_dev/command/model/configuration/_subresource_selector.py b/src/aaz_dev/command/model/configuration/_subresource_selector.py index 669b4885..5663b433 100644 --- a/src/aaz_dev/command/model/configuration/_subresource_selector.py +++ b/src/aaz_dev/command/model/configuration/_subresource_selector.py @@ -7,6 +7,7 @@ from ._fields import CMDVariantField from ._selector_index import CMDSelectorIndexField +from ._utils import CMDDiffLevelEnum class CMDSubresourceSelector(Model): @@ -37,6 +38,22 @@ def generate_args(self, ref_args): def reformat(self, **kwargs): raise NotImplementedError() + def _diff(self, old, level, diff): + if level >= CMDDiffLevelEnum.BreakingChange: + if self.var != old.var: + diff["var"] = f"{old.var} != {self.var}" + if self.ref != old.ref: + diff["ref"] = f"{old.ref} != {self.ref}" + return diff + + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + + diff = {} + diff = self._diff(old, level, diff) + return diff + class CMDJsonSubresourceSelector(CMDSubresourceSelector): """ @@ -107,3 +124,9 @@ def generate_args(self, ref_args): def reformat(self, **kwargs): self.json.reformat(**kwargs) + + def _diff(self, old, level, diff): + if level >= CMDDiffLevelEnum.BreakingChange: + if json_diff := self.json.diff(old.json, level): + diff["json"] = json_diff + return diff From cd7767850f9e180f93faa7984d183f60774c3172 Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Fri, 17 Nov 2023 16:48:25 +0800 Subject: [PATCH 2/9] Implement models required for client cfg diff --- .../controller/workspace_client_cfg_editor.py | 2 +- .../command/model/configuration/__init__.py | 2 +- .../command/model/configuration/_client.py | 73 ++++++++- .../command/model/configuration/_command.py | 145 +++++++++--------- .../command/model/configuration/_fields.py | 2 + .../command/model/configuration/_http.py | 102 +++++++++++- .../command/model/configuration/_operation.py | 34 ++++ .../command/model/configuration/_resource.py | 25 +++ .../command/model/configuration/_schema.py | 1 + .../tests/configuration_tests/test_client.py | 8 +- 10 files changed, 311 insertions(+), 83 deletions(-) diff --git a/src/aaz_dev/command/controller/workspace_client_cfg_editor.py b/src/aaz_dev/command/controller/workspace_client_cfg_editor.py index e9344044..6737d15a 100644 --- a/src/aaz_dev/command/controller/workspace_client_cfg_editor.py +++ b/src/aaz_dev/command/controller/workspace_client_cfg_editor.py @@ -39,7 +39,7 @@ def new_client_cfg(cls, plane, templates, auth, ref_cfg: ClientCfgReader = None) }, "auth": auth }) - cfg.endpoints.generate_params() + cfg.endpoints.prepare() ref_args = ref_cfg.cfg.arg_group.args if ref_cfg and ref_cfg.cfg.arg_group else None cfg.generate_args(ref_args=ref_args) if not ref_cfg or cfg.endpoints.diff(ref_cfg.cfg.endpoints, CMDDiffLevelEnum.Structure) or cfg.auth.diff(ref_cfg.cfg.auth, CMDDiffLevelEnum.Structure): diff --git a/src/aaz_dev/command/model/configuration/__init__.py b/src/aaz_dev/command/model/configuration/__init__.py index 1ddab9e6..2ce9fdf9 100644 --- a/src/aaz_dev/command/model/configuration/__init__.py +++ b/src/aaz_dev/command/model/configuration/__init__.py @@ -25,7 +25,7 @@ CMDArgPromptInput, CMDPasswordArgPromptInput from ._arg_builder import CMDArgBuilder from ._arg_group import CMDArgGroup -from ._client import CMDClientConfig, CMDClientEndpoints, CMDClientEndpointsByTemplate, CMDClientEndpointTemplate, CMDClientAuth, CMDClientAADAuthConfig +from ._client import CMDClientConfig, CMDClientEndpoints, CMDClientEndpointsByHttpOperation, CMDClientEndpointsByTemplate, CMDClientEndpointTemplate, CMDClientAuth, CMDClientAADAuthConfig from ._command import CMDCommand from ._command_group import CMDCommandGroup from ._condition import CMDConditionOperator, \ diff --git a/src/aaz_dev/command/model/configuration/_client.py b/src/aaz_dev/command/model/configuration/_client.py index 6f384389..0fb20bab 100644 --- a/src/aaz_dev/command/model/configuration/_client.py +++ b/src/aaz_dev/command/model/configuration/_client.py @@ -13,6 +13,11 @@ from ._utils import CMDArgBuildPrefix, CMDDiffLevelEnum from ._arg_builder import CMDArgBuilder from ._schema import CMDSchemaField, CMDStringSchema +from ._subresource_selector import CMDSubresourceSelector +from ._operation import CMDHttpOperation +from ._command import handle_duplicated_options +from ._resource import CMDResource + logger = logging.getLogger('backend') @@ -161,7 +166,7 @@ def reformat(self, **kwargs): pass @abc.abstractmethod - def generate_params(self): + def prepare(self): pass @abc.abstractmethod @@ -179,9 +184,6 @@ class CMDClientEndpointsByTemplate(CMDClientEndpoints): templates = ListType(ModelType(CMDClientEndpointTemplate), required=True, min_size=1) params = ListType(CMDSchemaField()) - class Options: - serialize_when_none = False - def reformat(self, **kwargs): for template in self.templates: template.reformat(**kwargs) @@ -217,7 +219,7 @@ def reformat(self, **kwargs): if self.params: self.params = sorted(self.params, key=lambda p: p.name) - def generate_params(self): + def prepare(self): params = {} for template in self.templates: for placeholder, required in template.iter_placeholders(): @@ -263,6 +265,67 @@ def diff(self, old, level): return diff +class CMDClientEndpointsByHttpOperation(CMDClientEndpoints): + TYPE_VALUE = 'http-operation' + + resource = ModelType(CMDResource, required=True) + + selector = PolyModelType( + CMDSubresourceSelector, + allow_subclasses=True, + serialized_name="selector", + deserialize_from="selector", + ) + + http_operation = ModelType( + CMDHttpOperation, + required=True, + serialized_name="httpOperation", + deserialize_from="httpOperation" + ) + + def reformat(self, **kwargs): + self.selector.reformat(**kwargs) + schema_cls_map = {} + self.operation.reformat(schema_cls_map=schema_cls_map, **kwargs) + for key, value in schema_cls_map.items(): + if value is None: + raise exceptions.VerificationError( + message=f"ReformatError: Schema Class '{key}' not defined.", + details=None + ) + + def prepare(self): + # TODO: remove unused schema based on selector + pass + + def generate_args(self, ref_args): + arguments = {} + has_subresource = False + if self.selector: + has_subresource = True + for arg in self.selector.generate_args(ref_args=ref_args): + if arg.var not in arguments: + arguments[arg.var] = arg + + for arg in self.operation.generate_args(ref_args=ref_args, has_subresource=has_subresource): + if arg.var not in arguments: + arguments[arg.var] = arg + arguments = handle_duplicated_options( + arguments, has_subresource=has_subresource, operation_id=self.operation.operation_id) + return [arg for arg in arguments.values()] + + def diff(self, old, level): + diff = {} + if resource_diff := self.resource.diff(old.resource): + diff["resource"] = resource_diff + if selector_diff := self.selector.diff(old.selector): + diff["selector"] = selector_diff + if operation_diff := self.http_operation.diff(old.http_operation): + diff["http_operation"] = operation_diff + return diff + + class CMDClientConfig(Model): # this property is used to manage the client config version. version = UTCDateTimeType(required=True) diff --git a/src/aaz_dev/command/model/configuration/_command.py b/src/aaz_dev/command/model/configuration/_command.py index f945b674..11728c02 100644 --- a/src/aaz_dev/command/model/configuration/_command.py +++ b/src/aaz_dev/command/model/configuration/_command.py @@ -81,7 +81,8 @@ def generate_args(self, ref_args=None, ref_options=None): arg.options = [*ref_options[arg.var]] arguments[arg.var] = arg - arguments = self._handle_duplicated_options(arguments) + arguments = handle_duplicated_options( + arguments, has_subresource=has_subresource, operation_id=self.operations[-1].operation_id) self.arg_groups = self._build_arg_groups(arguments) def generate_outputs(self, ref_outputs=None, pageable=None): @@ -248,38 +249,6 @@ def link(self): self.schema_cls_register_map = schema_cls_register_map - def _handle_duplicated_options(self, arguments): - # check argument with duplicated option names - dropped_args = set() - used_args = set() - for arg in arguments.values(): - used_args.add(arg.var) - if arg.var in dropped_args or not arg.options: - continue - r_arg = None - for v in arguments.values(): - if v.var in used_args or v.var in dropped_args or arg.var == v.var or not v.options: - continue - if not set(arg.options).isdisjoint(v.options): - r_arg = v - break - if r_arg: - # check whether need to replace argument - if self._can_replace_argument(r_arg, arg): - arg.ref_schema.arg = r_arg.var - dropped_args.add(arg.var) - elif self._can_replace_argument(arg, r_arg): - r_arg.ref_schema.arg = arg.var - dropped_args.add(r_arg.var) - else: - # warning developer handle duplicated options - logger.warning( - f"Duplicated Option Value: {set(arg.options).intersection(r_arg.options)} : " - f"{arg.var} with {r_arg.var} : {self.operations[-1].operation_id}" - ) - - return [arg for var, arg in arguments.items() if var not in dropped_args] - @staticmethod def _build_arg_groups(arguments): # build argument groups @@ -299,44 +268,6 @@ def _build_arg_groups(arguments): groups.append(group) return groups or None - def _can_replace_argument(self, arg, old_arg): - arg_prefix = arg.var.split('.')[0] - old_prefix = old_arg.var.split('.')[0] - - if old_prefix in (CMDArgBuildPrefix.Query, CMDArgBuildPrefix.Header, CMDArgBuildPrefix.Path): - # replace argument should only be in body - return False - - if arg_prefix in (CMDArgBuildPrefix.Query, CMDArgBuildPrefix.Header): - # only support path argument to replace - return False - - elif arg_prefix == CMDArgBuildPrefix.Path: - # path argument - if self.subresource_selector is not None: - return False - - arg_schema_required = arg.ref_schema.required - arg_schema_name = arg.ref_schema.name - try: - # temporary assign required and name for diff - arg.ref_schema.required = old_arg.ref_schema.required - if old_arg.ref_schema.name == "name" and "name" in arg.options: - arg.ref_schema.name = "name" - diff = arg.ref_schema.diff(old_arg.ref_schema, level=CMDDiffLevelEnum.Structure) - if diff: - return False - return True - finally: - arg.ref_schema.name = arg_schema_name - arg.ref_schema.required = arg_schema_required - else: - # body argument - diff = arg.ref_schema.diff(old_arg.ref_schema, level=CMDDiffLevelEnum.Structure) - if diff: - return False - return True - @staticmethod def _build_output_type_schema(schema): if isinstance(schema, CMDClsSchemaBase): @@ -388,3 +319,75 @@ def _build_output_type_by_subresource_selector(subresource_selector): else: raise NotImplementedError() return output + + +def handle_duplicated_options(arguments, has_subresource, operation_id): + # check argument with duplicated option names + dropped_args = set() + used_args = set() + for arg in arguments.values(): + used_args.add(arg.var) + if arg.var in dropped_args or not arg.options: + continue + r_arg = None + for v in arguments.values(): + if v.var in used_args or v.var in dropped_args or arg.var == v.var or not v.options: + continue + if not set(arg.options).isdisjoint(v.options): + r_arg = v + break + if r_arg: + # check whether you need to replace argument + if _can_replace_argument(r_arg, arg, has_subresource): + arg.ref_schema.arg = r_arg.var + dropped_args.add(arg.var) + elif _can_replace_argument(arg, r_arg, has_subresource): + r_arg.ref_schema.arg = arg.var + dropped_args.add(r_arg.var) + else: + # warning developer handle duplicated options + logger.warning( + f"Duplicated Option Value: {set(arg.options).intersection(r_arg.options)} : " + f"{arg.var} with {r_arg.var} : {operation_id}" + ) + + return [arg for var, arg in arguments.items() if var not in dropped_args] + + +def _can_replace_argument(arg, old_arg, has_subresource): + arg_prefix = arg.var.split('.')[0] + old_prefix = old_arg.var.split('.')[0] + + if old_prefix in (CMDArgBuildPrefix.Query, CMDArgBuildPrefix.Header, CMDArgBuildPrefix.Path): + # replace argument should only be in body + return False + + if arg_prefix in (CMDArgBuildPrefix.Query, CMDArgBuildPrefix.Header): + # only support path argument to replace + return False + + elif arg_prefix == CMDArgBuildPrefix.Path: + # path argument + if has_subresource: + return False + + arg_schema_required = arg.ref_schema.required + arg_schema_name = arg.ref_schema.name + try: + # temporary assign required and name for diff + arg.ref_schema.required = old_arg.ref_schema.required + if old_arg.ref_schema.name == "name" and "name" in arg.options: + arg.ref_schema.name = "name" + diff = arg.ref_schema.diff(old_arg.ref_schema, level=CMDDiffLevelEnum.Structure) + if diff: + return False + return True + finally: + arg.ref_schema.name = arg_schema_name + arg.ref_schema.required = arg_schema_required + else: + # body argument + diff = arg.ref_schema.diff(old_arg.ref_schema, level=CMDDiffLevelEnum.Structure) + if diff: + return False + return True diff --git a/src/aaz_dev/command/model/configuration/_fields.py b/src/aaz_dev/command/model/configuration/_fields.py index 00214e23..57af6093 100644 --- a/src/aaz_dev/command/model/configuration/_fields.py +++ b/src/aaz_dev/command/model/configuration/_fields.py @@ -100,11 +100,13 @@ class CMDVersionField(StringType): def __init__(self, *args, **kwargs): super(CMDVersionField, self).__init__(*args, **kwargs) + class CMDConfirmation(StringType): def __int__(self, *args, **kwargs): super(CMDConfirmation, self).__init__(*args, **kwargs) + class CMDResourceIdField(StringType): def __init__(self, *args, **kwargs): diff --git a/src/aaz_dev/command/model/configuration/_http.py b/src/aaz_dev/command/model/configuration/_http.py index 2b59ac6e..1db9c569 100644 --- a/src/aaz_dev/command/model/configuration/_http.py +++ b/src/aaz_dev/command/model/configuration/_http.py @@ -8,7 +8,7 @@ from ._fields import CMDVariantField, CMDBooleanField, CMDURLPathField, CMDDescriptionField from ._http_request_body import CMDHttpRequestBody from ._http_response_body import CMDHttpResponseBody -from ._schema import CMDSchemaField +from ._schema import CMDSchemaField, _diff_props from ._arg_builder import CMDArgBuilder from ._arg import CMDResourceGroupNameArg, CMDSubscriptionIdArg, CMDResourceLocationArg from ._utils import CMDDiffLevelEnum @@ -34,6 +34,19 @@ def reformat(self, **kwargs): const.reformat(**kwargs) self.consts = sorted(self.consts, key=lambda c: c.name) + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + + params_diff = _diff_props(self.params, old.params, level) + if params_diff: + diff["params"] = params_diff + consts_diff = _diff_props(self.consts, old.consts, level) + if consts_diff: + diff["consts"] = consts_diff + return diff + class CMDHttpRequestPath(CMDHttpRequestArgs): @@ -126,6 +139,13 @@ def generate_args(self, ref_args): args.extend(builder.get_args()) return args + def diff(self, old, level): + diff = super().diff(old, level) + if level >= CMDDiffLevelEnum.BreakingChange: + if self.client_request_id != old.client_request_id: + diff["client_request_id"] = f"{old.client_request_id} != {self.client_request_id}" + return diff + class CMDHttpRequest(Model): # properties as tags @@ -166,6 +186,39 @@ def register_cls(self, **kwargs): if self.body: self.body.register_cls(**kwargs) + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if self.method != old.method: + diff["method"] = f"{old.method} != {self.method}" + if (not self.path) != (not old.path): + diff["path"] = "Miss path" if old.path else "New path" + elif self.path: + path_diff = self.path.diff(old.path, level) + if path_diff: + diff["path"] = path_diff + if (not self.query) != (not old.query): + diff["query"] = "Miss query" if old.query else "New query" + elif self.query: + query_diff = self.query.diff(old.query, level) + if query_diff: + diff["query"] = query_diff + if (not self.header) != (not old.header): + diff["header"] = "Miss header" if old.header else "New header" + elif self.header: + header_diff = self.header.diff(old.header, level) + if header_diff: + diff["header"] = header_diff + if (not self.body) != (not old.body): + diff["body"] = "Miss request body" if old.body else "New request body" + elif self.body: + body_diff = self.body.diff(old.body, level) + if body_diff: + diff["body"] = body_diff + return diff + class CMDHttpResponseHeaderItem(Model): # properties as tags @@ -324,3 +377,50 @@ def register_cls(self, **kwargs): if response.is_error: continue response.register_cls(**kwargs) + + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if self.path != old.path: + diff["path"] = f"{old.path} != {self.path}" + if (not self.request) != (not old.request): + diff["request"] = "Miss request" if old.request else "New request" + elif self.request: + request_diff = self.request.diff(old.request, level) + if request_diff: + diff["request"] = request_diff + + responses_diff = _diff_responses(self.responses or [], old.responses or [], level) + if responses_diff: + diff["responses"] = responses_diff + + return diff + + +def _diff_responses(responses, old_responses, level): + diff = {} + + def _build_key(_r): + _codes = ','.join(_r.status_codes) if _r.status_codes else '' + _error = 'error' if _r.is_error else '' + return '|'.join([_codes, _error]) + + if level >= CMDDiffLevelEnum.BreakingChange: + responses_dict = {_build_key(resp): resp for resp in responses} + for old_resp in old_responses: + old_resp_key = _build_key(old_resp) + if old_resp_key not in responses_dict: + diff[old_resp_key] = "Miss response" + else: + resp = responses_dict.pop(old_resp_key) + resp_diff = resp.diff(old_resp, level) + if resp_diff: + diff[old_resp_key] = resp_diff + + for resp in responses_dict.values(): + resp_key = _build_key(resp) + diff[resp_key] = "New response" + + return diff diff --git a/src/aaz_dev/command/model/configuration/_operation.py b/src/aaz_dev/command/model/configuration/_operation.py index 5a392038..7fb09f6b 100644 --- a/src/aaz_dev/command/model/configuration/_operation.py +++ b/src/aaz_dev/command/model/configuration/_operation.py @@ -6,6 +6,7 @@ from ._instance_update import CMDInstanceUpdateAction from ._instance_create import CMDInstanceCreateAction from ._instance_delete import CMDInstanceDeleteAction +from ._utils import CMDDiffLevelEnum class CMDOperation(Model): @@ -54,6 +55,15 @@ class CMDHttpOperationLongRunning(Model): deserialize_from='finalStateVia', ) + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if self.final_state_via != old.final_state_via: + diff["final_state_via"] = f"{old.final_state_via} != {self.final_state_via}" + return diff + class CMDHttpOperation(CMDOperation): POLYMORPHIC_KEY = "http" @@ -85,6 +95,30 @@ def reformat(self, **kwargs): def register_cls(self, **kwargs): self.http.register_cls(**kwargs) + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if (not self.long_running) != (not old.long_running): + diff["long_running"] = "New long_running" if self.long_running else "Miss long_running" + elif self.long_running: + if lr_diff := self.long_running.diff(old.long_running, level): + diff["long_running"] = lr_diff + + if level >= CMDDiffLevelEnum.Structure: + if self.operation_id != old.operation_id: + diff["operation_id"] = f"{old.operation_id} != {self.operation_id}" + + if level >= CMDDiffLevelEnum.All: + if self.description != old.description: + diff["description"] = f"{old.description} != {self.description}" + + http_diff = self.http.diff(old.http, level) + if http_diff: + diff["http"] = http_diff + return diff + class CMDInstanceCreateOperation(CMDOperation): POLYMORPHIC_KEY = "instanceCreate" diff --git a/src/aaz_dev/command/model/configuration/_resource.py b/src/aaz_dev/command/model/configuration/_resource.py index 459a76c2..d700e447 100644 --- a/src/aaz_dev/command/model/configuration/_resource.py +++ b/src/aaz_dev/command/model/configuration/_resource.py @@ -2,6 +2,7 @@ from schematics.types import StringType from ._fields import CMDResourceIdField, CMDVersionField +from ._utils import CMDDiffLevelEnum from utils.base64 import b64decode_str @@ -31,3 +32,27 @@ def rp_name(self): @property def swagger_path(self): return b64decode_str(self.swagger.split("/Paths/")[1].split('/')[0]) + + def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" + diff = {} + if level >= CMDDiffLevelEnum.BreakingChange: + if self.id != old.id: + diff["id"] = f"{old.id} != {self.id}" + if self.version != old.version: + diff["version"] = f"{old.version} != {self.version}" + if self.subresource != old.subresource: + diff["subresource"] = f"{old.subresource} != {self.subresource}" + if self.plane != old.plane: + diff["plane"] = f"{old.plane} != {self.plane}" + if self.rp_name != old.rp_name: + diff["rp_name"] = f"{old.rp_name} != {self.rp_name}" + + if level >= CMDDiffLevelEnum.Structure: + if self.mod_names != old.mod_names: + diff["mod_names"] = f"{old.mod_names} != {self.mod_names}" + if self.swagger_path != old.swagger_path: + diff["swagger_path"] = f"{old.swagger_path} != {self.swagger_path}" + + return diff diff --git a/src/aaz_dev/command/model/configuration/_schema.py b/src/aaz_dev/command/model/configuration/_schema.py index 2e187ad7..344dccf9 100644 --- a/src/aaz_dev/command/model/configuration/_schema.py +++ b/src/aaz_dev/command/model/configuration/_schema.py @@ -788,6 +788,7 @@ def get_safe_value(self): safe_value = re.sub(r'[^A-Za-z0-9_-]', '_', self.value) return safe_value + # additionalProperties class CMDObjectSchemaAdditionalProperties(Model): ARG_TYPE = CMDObjectArgAdditionalProperties diff --git a/src/aaz_dev/command/tests/configuration_tests/test_client.py b/src/aaz_dev/command/tests/configuration_tests/test_client.py index 1a6bbe4d..f8bb858c 100644 --- a/src/aaz_dev/command/tests/configuration_tests/test_client.py +++ b/src/aaz_dev/command/tests/configuration_tests/test_client.py @@ -30,7 +30,7 @@ def test_client_config(self): }] }) cfg.version = datetime.utcnow() - cfg.endpoints.generate_params() + cfg.endpoints.prepare() cfg.generate_args() cfg.reformat() @@ -63,7 +63,7 @@ def test_client_config_with_params(self): }] }) cfg.version = datetime.utcnow() - cfg.endpoints.generate_params() + cfg.endpoints.prepare() cfg.generate_args() cfg.reformat() @@ -99,7 +99,7 @@ def test_client_config_reformat(self): }] }) cfg.version = datetime.utcnow() - cfg.endpoints.generate_params() + cfg.endpoints.prepare() cfg.generate_args() with self.assertRaises(exceptions.VerificationError): cfg.reformat() @@ -127,7 +127,7 @@ def test_client_config_generate_args(self): ] }) cfg.version = datetime.utcnow() - cfg.endpoints.generate_params() + cfg.endpoints.prepare() cfg.generate_args() with self.assertRaises(exceptions.VerificationError): From 7f782b24c39a80192f2aac425a651c1d214313df Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Tue, 21 Nov 2023 12:05:35 +0800 Subject: [PATCH 3/9] Implement API for dynamic endpoint of client configuration --- src/aaz_dev/command/api/editor.py | 15 +- .../controller/workspace_cfg_editor.py | 65 +++++- .../controller/workspace_client_cfg_editor.py | 29 ++- .../command/controller/workspace_manager.py | 54 ++++- .../command/model/configuration/__init__.py | 5 +- .../command/model/configuration/_arg.py | 1 + .../command/model/configuration/_client.py | 29 ++- .../command/model/configuration/_content.py | 9 +- .../command/model/configuration/_http.py | 44 ++-- .../model/configuration/_http_request_body.py | 6 +- .../model/configuration/_instance_create.py | 8 +- .../model/configuration/_instance_delete.py | 6 +- .../model/configuration/_instance_update.py | 6 +- .../command/model/configuration/_operation.py | 18 +- .../model/configuration/_selector_index.py | 28 ++- .../configuration/_subresource_selector.py | 12 +- .../command/model/configuration/_utils.py | 12 +- .../command/tests/api_tests/test_editor.py | 204 +++++++++++++++++- .../tests/configuration_tests/test_xml.py | 8 +- .../swagger/controller/command_generator.py | 54 +++-- .../swagger/controller/specs_manager.py | 5 + .../test_command_generator.py | 20 +- .../swagger/tests/schema_tests/test_schema.py | 4 +- 23 files changed, 518 insertions(+), 124 deletions(-) diff --git a/src/aaz_dev/command/api/editor.py b/src/aaz_dev/command/api/editor.py index 7ead2ea1..aa5ec18e 100644 --- a/src/aaz_dev/command/api/editor.py +++ b/src/aaz_dev/command/api/editor.py @@ -118,9 +118,18 @@ def editor_workspace_client_config(name): raise exceptions.ResourceNotFind("Client configuration not exist") elif request.method == "POST": data = request.get_json() - if 'templates' not in data or 'auth' not in data: - raise exceptions.InvalidAPIUsage("Invalid request") - cfg_editor = manager.create_cfg_editor(templates=data['templates'], auth=data['auth']) + if 'auth' not in data: + raise exceptions.InvalidAPIUsage("Invalid request: auth info is required.") + if 'templates' not in data and 'resource' not in data: + raise exceptions.InvalidAPIUsage("Invalid request: templates or resource is required for endpoints.") + if 'resource' in data: + if 'id' not in data['resource'] or 'version' not in data['resource'] or 'module' not in data['resource'] or 'subresource' not in data['resource']: + raise exceptions.InvalidAPIUsage("Invalid request") + cfg_editor = manager.create_cfg_editor( + auth=data['auth'], + templates=data.get('templates', None), + arm_resource=data.get('resource', None), + ) manager.save() else: raise NotImplementedError() diff --git a/src/aaz_dev/command/controller/workspace_cfg_editor.py b/src/aaz_dev/command/controller/workspace_cfg_editor.py index 3d7f43a2..1fd9857b 100644 --- a/src/aaz_dev/command/controller/workspace_cfg_editor.py +++ b/src/aaz_dev/command/controller/workspace_cfg_editor.py @@ -1322,6 +1322,16 @@ def _build_subresource_selector(cls, response_json, update_json, subresource_idx return selector + @classmethod + def _build_simple_index_base(cls, schema, idx, index=None, **kwargs): + if index is None: + if not isinstance(schema, CMDSimpleIndexBase.supported_schema_types): + raise NotImplementedError(f"Not support schema '{type(schema)}'") + index = CMDSimpleIndexBase() + if idx: + raise exceptions.InvalidAPIUsage("Simple schema is not support feature index") + return index + @classmethod def _build_object_index_base(cls, schema, idx, index=None, prune=False, **kwargs): assert isinstance(schema, CMDObjectSchemaBase) @@ -1349,7 +1359,7 @@ def _build_object_index_base(cls, schema, idx, index=None, prune=False, **kwargs elif isinstance(prop, CMDArraySchema): return cls._build_array_index(prop, remain_idx, name=name, **kwargs) else: - raise NotImplementedError() + return cls._build_simple_index(prop, remain_idx, name=name, **kwargs) else: name = prop.name if isinstance(prop, CMDClsSchema): @@ -1361,7 +1371,8 @@ def _build_object_index_base(cls, schema, idx, index=None, prune=False, **kwargs index.prop = cls._build_array_index(prop, remain_idx, name=name, **kwargs) break else: - raise NotImplementedError() + index.prop = cls._build_simple_index(prop, remain_idx, name=name, **kwargs) + break if schema.discriminators: for disc in schema.discriminators: @@ -1397,7 +1408,7 @@ def _build_array_index_base(cls, schema, idx, index=None, **kwargs): if not identifier_names and isinstance(item, CMDObjectSchemaBase): prop_names = {prop.name for prop in item.props} if 'id' in prop_names and 'name' in prop_names: - # use name as default identifier when schema containes 'id' property + # use name as default identifier when schema contains 'id' property identifier_names = ['name'] if identifier_names: @@ -1424,10 +1435,18 @@ def _build_array_index_base(cls, schema, idx, index=None, **kwargs): elif isinstance(item, CMDArraySchemaBase): index.item = cls._build_array_index_base(item, remain_idx, **kwargs) else: - raise NotImplementedError() + index.item = cls._build_simple_index_base(item, remain_idx, **kwargs) return index + @classmethod + def _build_simple_index(cls, schema, idx, name, **kwargs): + if not isinstance(schema, CMDSimpleIndex.supported_schema_types): + raise NotImplementedError(f"Not support schema '{type(schema)}'") + index = CMDSimpleIndex() + index.name = name + return cls._build_simple_index_base(schema, idx, index=index, **kwargs) + @classmethod def _build_object_index(cls, schema, idx, name, **kwargs): index = CMDObjectIndex() @@ -1468,7 +1487,8 @@ def _build_object_index_discriminator(cls, schema, idx, **kwargs): index.prop = cls._build_array_index(prop, remain_idx, name, **kwargs) break else: - raise NotImplementedError() + index.prop = cls._build_simple_index(prop, remain_idx, name, **kwargs) + break if schema.discriminators: for disc in schema.discriminators: @@ -1496,7 +1516,7 @@ def _build_object_index_additional_prop(cls, schema, idx, **kwargs): elif isinstance(item, CMDArraySchemaBase): index.item = cls._build_array_index_base(item, idx, **kwargs) else: - raise NotImplementedError() + index.item = cls._build_simple_index_base(item, idx, **kwargs) return index @@ -1549,3 +1569,36 @@ def fork_schema_in_json(cls, js, idx, reference_cls_map): raise exceptions.InvalidAPIUsage(f"Always miss class implements for {miss_implement_cls_names}") return schema + + +def build_endpoint_selector_for_client_config(get_op, subresource_idx, **kwargs): + # find response body + response_json = None + for response in get_op.http.responses: + if response.is_error: + continue + if not isinstance(response.body, CMDHttpResponseJsonBody): + continue + if response.body.json.var == CMDBuildInVariants.EndpointInstance: + response_json = response.body.json + break + + assert isinstance(response_json, CMDResponseJson) + idx = WorkspaceCfgEditor.idx_to_list(subresource_idx) + selector = CMDJsonSubresourceSelector() + selector.ref = response_json.var + selector.var = CMDBuildInVariants.Endpoint + if isinstance(response_json.schema, CMDObjectSchemaBase): + index = CMDObjectIndex() + index.name = 'response' + selector.json = WorkspaceCfgEditor._build_object_index_base( + response_json.schema, idx, index=index, **kwargs) + elif isinstance(response_json.schema, CMDArraySchemaBase): + index = CMDArrayIndex() + index.name = 'response' + selector.json = WorkspaceCfgEditor._build_array_index_base( + response_json.schema, idx, index=index, **kwargs) + else: + raise NotImplementedError(f"Not support schema {type(response_json.schema)}") + + return selector diff --git a/src/aaz_dev/command/controller/workspace_client_cfg_editor.py b/src/aaz_dev/command/controller/workspace_client_cfg_editor.py index 6737d15a..315d0a01 100644 --- a/src/aaz_dev/command/controller/workspace_client_cfg_editor.py +++ b/src/aaz_dev/command/controller/workspace_client_cfg_editor.py @@ -5,7 +5,7 @@ from .client_cfg_reader import ClientCfgReader from .workspace_helper import ArgumentUpdateMixin -from command.model.configuration import CMDClientConfig, CMDDiffLevelEnum +from command.model.configuration import CMDClientConfig, CMDDiffLevelEnum, CMDClientEndpointsByTemplate, CMDClientEndpointsByHttpOperation, CMDClientEndpoints logger = logging.getLogger('backend') @@ -30,16 +30,13 @@ def load_client_cfg(cls, ws_folder): return cfg_editor @classmethod - def new_client_cfg(cls, plane, templates, auth, ref_cfg: ClientCfgReader = None): + def new_client_cfg(cls, plane, auth, endpoints, ref_cfg: ClientCfgReader = None): + assert isinstance(endpoints, CMDClientEndpoints) cfg = CMDClientConfig(raw_data={ "plane": plane, - "endpoints": { - "type": "templates", - "templates": templates - }, "auth": auth }) - cfg.endpoints.prepare() + cfg.endpoints = endpoints ref_args = ref_cfg.cfg.arg_group.args if ref_cfg and ref_cfg.cfg.arg_group else None cfg.generate_args(ref_args=ref_args) if not ref_cfg or cfg.endpoints.diff(ref_cfg.cfg.endpoints, CMDDiffLevelEnum.Structure) or cfg.auth.diff(ref_cfg.cfg.auth, CMDDiffLevelEnum.Structure): @@ -52,6 +49,24 @@ def new_client_cfg(cls, plane, templates, auth, ref_cfg: ClientCfgReader = None) cfg_editor.reformat() return cfg_editor + @classmethod + def new_client_endpoints_by_template(cls, templates): + endpoints = CMDClientEndpointsByTemplate(raw_data={ + "templates": templates + }) + endpoints.prepare() + return endpoints + + @classmethod + def new_client_endpoints_by_http_operation(cls, resource, operation, selector): + endpoints = CMDClientEndpointsByHttpOperation(raw_data={ + "resource": resource, + "selector": selector, + "operation": operation, + }) + endpoints.prepare() + return endpoints + def __init__(self, cfg): super().__init__(cfg) diff --git a/src/aaz_dev/command/controller/workspace_manager.py b/src/aaz_dev/command/controller/workspace_manager.py index 8ed2e955..da8de122 100644 --- a/src/aaz_dev/command/controller/workspace_manager.py +++ b/src/aaz_dev/command/controller/workspace_manager.py @@ -12,9 +12,9 @@ from utils.config import Config from utils.plane import PlaneEnum from .specs_manager import AAZSpecsManager -from .workspace_cfg_editor import WorkspaceCfgEditor +from .workspace_cfg_editor import WorkspaceCfgEditor, build_endpoint_selector_for_client_config from .workspace_client_cfg_editor import WorkspaceClientCfgEditor -from command.model.configuration import CMDHelp, CMDResource, CMDCommandExample, CMDArg, CMDCommand +from command.model.configuration import CMDHelp, CMDResource, CMDCommandExample, CMDArg, CMDCommand, CMDBuildInVariants logger = logging.getLogger('backend') @@ -550,9 +550,8 @@ def add_new_resources_by_swagger(self, mod_names, version, resources): raise exceptions.InvalidAPIUsage( f"Resource already added in Workspace: {r['id']}") # convert resource to swagger resource - swagger_resource = self.swagger_specs.get_module_manager( - plane=self.ws.plane, mod_names=mod_names - ).get_resource_in_version(r['id'], version) + swagger_resource = self.swagger_specs.get_swagger_resource( + plane=self.ws.plane, mod_names=mod_names, resource_id=r['id'], version=version) swagger_resources.append(swagger_resource) resource_options.append(r.get("options", {})) used_resource_ids.update(r['id']) @@ -566,7 +565,7 @@ def add_new_resources_by_swagger(self, mod_names, version, resources): for resource, options in zip(swagger_resources, resource_options): try: command_group = self.swagger_command_generator.create_draft_command_group( - resource, **options) + resource, instance_var=CMDBuildInVariants.Instance, **options) except InvalidSwaggerValueError as err: raise exceptions.InvalidAPIUsage( message=str(err) @@ -688,7 +687,7 @@ def reload_swagger_resources(self, resources): options['update_by'] = update_by try: command_group = self.swagger_command_generator.create_draft_command_group( - swagger_resource, **options) + swagger_resource, instance_var=CMDBuildInVariants.Instance, **options) except InvalidSwaggerValueError as err: raise exceptions.InvalidAPIUsage( message=str(err) @@ -1049,9 +1048,46 @@ def find_similar_args(self, *cmd_names, arg): # client config - def create_cfg_editor(self, templates, auth): + def create_cfg_editor(self, auth, templates=None, arm_resource=None): ref_cfg = self.load_client_cfg_editor() - self._client_cfg_editor = WorkspaceClientCfgEditor.new_client_cfg(plane=self.ws.plane, templates=templates, auth=auth, ref_cfg=ref_cfg) + if templates: + endpoints = WorkspaceClientCfgEditor.new_client_endpoints_by_template(templates) + elif arm_resource: + # arm_resrouce should have these keys: "module", "version", "id", "subresource" + mod_names = arm_resource['module'] + version = arm_resource['version'] + resource_id = arm_resource['id'] + subresource = arm_resource['subresource'] + swagger_resource = self.swagger_specs.get_swagger_resource( + plane=PlaneEnum.Mgmt, mod_names=mod_names, resource_id=resource_id, version=version) + if 'get' not in set(swagger_resource.operations.values()): + raise exceptions.InvalidAPIUsage(f"The resource doesn't has 'get' method: {resource_id}") + self.swagger_command_generator.load_resources([swagger_resource]) + + resource = swagger_resource.to_cmd() + resource.subresource = subresource + + # build get operation by draft command + get_op = self.swagger_command_generator.create_draft_command_group( + swagger_resource, instance_var=CMDBuildInVariants.EndpointInstance, methods=('get',) + ).commands[0].operations[0] + + selector = build_endpoint_selector_for_client_config(get_op, subresource_idx=resource.subresource) + + endpoints = WorkspaceClientCfgEditor.new_client_endpoints_by_http_operation( + resource=resource, + selector=selector, + operation=get_op, + ) + else: + raise NotImplementedError() + + self._client_cfg_editor = WorkspaceClientCfgEditor.new_client_cfg( + plane=self.ws.plane, + auth=auth, + endpoints=endpoints, + ref_cfg=ref_cfg, + ) return self._client_cfg_editor def load_client_cfg_editor(self, reload=False): diff --git a/src/aaz_dev/command/model/configuration/__init__.py b/src/aaz_dev/command/model/configuration/__init__.py index 2ce9fdf9..6b475de4 100644 --- a/src/aaz_dev/command/model/configuration/__init__.py +++ b/src/aaz_dev/command/model/configuration/__init__.py @@ -78,7 +78,8 @@ CMDIdentityObjectSchemaBase, CMDIdentityObjectSchema, \ CMDArraySchemaBase, CMDArraySchema from ._selector_index import CMDSelectorIndexBase, CMDSelectorIndex, CMDObjectIndexDiscriminator, \ - CMDObjectIndexAdditionalProperties, CMDObjectIndexBase, CMDObjectIndex, CMDArrayIndexBase, CMDArrayIndex + CMDObjectIndexAdditionalProperties, CMDObjectIndexBase, CMDObjectIndex, CMDArrayIndexBase, CMDArrayIndex, \ + CMDSimpleIndexBase, CMDSimpleIndex from ._subresource_selector import CMDSubresourceSelector, CMDJsonSubresourceSelector -from ._utils import CMDDiffLevelEnum, DEFAULT_CONFIRMATION_PROMPT +from ._utils import CMDDiffLevelEnum, DEFAULT_CONFIRMATION_PROMPT, CMDBuildInVariants from ._xml import XMLSerializer diff --git a/src/aaz_dev/command/model/configuration/_arg.py b/src/aaz_dev/command/model/configuration/_arg.py index 61bda168..c4750d81 100644 --- a/src/aaz_dev/command/model/configuration/_arg.py +++ b/src/aaz_dev/command/model/configuration/_arg.py @@ -337,6 +337,7 @@ def get_unwrapped(self, **kwargs): uninherent.update(kwargs) return super().get_unwrapped(**uninherent) + # string class CMDStringArgBase(CMDArgBase): TYPE_VALUE = "string" diff --git a/src/aaz_dev/command/model/configuration/_client.py b/src/aaz_dev/command/model/configuration/_client.py index 0fb20bab..9e1d48d5 100644 --- a/src/aaz_dev/command/model/configuration/_client.py +++ b/src/aaz_dev/command/model/configuration/_client.py @@ -136,7 +136,7 @@ def diff(self, old, level): class CMDClientEndpoints(Model): # properties as tags - TYPE_VALUE = None # types: "template", + TYPE_VALUE = None # types: "template", "http-operation" class Options: serialize_when_none = False @@ -179,7 +179,7 @@ def diff(self, old, level): class CMDClientEndpointsByTemplate(CMDClientEndpoints): - TYPE_VALUE = 'templates' + TYPE_VALUE = 'template' templates = ListType(ModelType(CMDClientEndpointTemplate), required=True, min_size=1) params = ListType(CMDSchemaField()) @@ -277,11 +277,11 @@ class CMDClientEndpointsByHttpOperation(CMDClientEndpoints): deserialize_from="selector", ) - http_operation = ModelType( + operation = ModelType( CMDHttpOperation, required=True, - serialized_name="httpOperation", - deserialize_from="httpOperation" + serialized_name="operation", + deserialize_from="operation" ) def reformat(self, **kwargs): @@ -304,16 +304,23 @@ def generate_args(self, ref_args): has_subresource = False if self.selector: has_subresource = True - for arg in self.selector.generate_args(ref_args=ref_args): + for arg in self.selector.generate_args( + ref_args=ref_args, + var_prefix=CMDArgBuildPrefix.ClientEndpoint + ): if arg.var not in arguments: arguments[arg.var] = arg - for arg in self.operation.generate_args(ref_args=ref_args, has_subresource=has_subresource): + for arg in self.operation.generate_args( + ref_args=ref_args, + has_subresource=has_subresource, + var_prefix=CMDArgBuildPrefix.ClientEndpoint + ): if arg.var not in arguments: arguments[arg.var] = arg - arguments = handle_duplicated_options( + + return handle_duplicated_options( arguments, has_subresource=has_subresource, operation_id=self.operation.operation_id) - return [arg for arg in arguments.values()] def diff(self, old, level): diff = {} @@ -321,8 +328,8 @@ def diff(self, old, level): diff["resource"] = resource_diff if selector_diff := self.selector.diff(old.selector): diff["selector"] = selector_diff - if operation_diff := self.http_operation.diff(old.http_operation): - diff["http_operation"] = operation_diff + if operation_diff := self.operation.diff(old.operation): + diff["operation"] = operation_diff return diff diff --git a/src/aaz_dev/command/model/configuration/_content.py b/src/aaz_dev/command/model/configuration/_content.py index 4c4bb2ee..ec36df5e 100644 --- a/src/aaz_dev/command/model/configuration/_content.py +++ b/src/aaz_dev/command/model/configuration/_content.py @@ -20,11 +20,16 @@ class CMDRequestJson(Model): class Options: serialize_when_none = False - def generate_args(self, ref_args, is_update_action=False): + def generate_args(self, ref_args, var_prefix=None, is_update_action=False): if not self.schema: return [] assert isinstance(self.schema, CMDSchema) - builder = CMDArgBuilder.new_builder(schema=self.schema, ref_args=ref_args, is_update_action=is_update_action) + builder = CMDArgBuilder.new_builder( + schema=self.schema, + ref_args=ref_args, + var_prefix=var_prefix, + is_update_action=is_update_action + ) args = builder.get_args() return args diff --git a/src/aaz_dev/command/model/configuration/_http.py b/src/aaz_dev/command/model/configuration/_http.py index 1db9c569..0a24bbd0 100644 --- a/src/aaz_dev/command/model/configuration/_http.py +++ b/src/aaz_dev/command/model/configuration/_http.py @@ -50,7 +50,7 @@ def diff(self, old, level): class CMDHttpRequestPath(CMDHttpRequestArgs): - def generate_args(self, path, ref_args, has_subresource): + def generate_args(self, path, ref_args, has_subresource, var_prefix=None): try: id_parts = parse_resource_id(path) resource_id(**id_parts) @@ -62,7 +62,12 @@ def generate_args(self, path, ref_args, has_subresource): resource_name = None args = [] if self.params: - var_prefix = CMDArgBuildPrefix.Path + if var_prefix: + if not var_prefix.endswith("$"): + var_prefix += '.' + var_prefix += CMDArgBuildPrefix.Path[1:] # PATH + else: + var_prefix = CMDArgBuildPrefix.Path for param in self.params: id_part = None placeholder = '{' + param.name + '}' @@ -112,12 +117,18 @@ def generate_args(self, path, ref_args, has_subresource): class CMDHttpRequestQuery(CMDHttpRequestArgs): - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): args = [] if self.params: + if var_prefix: + if not var_prefix.endswith("$"): + var_prefix += '.' + var_prefix += CMDArgBuildPrefix.Query[1:] # Query + else: + var_prefix = CMDArgBuildPrefix.Query for param in self.params: builder = CMDArgBuilder.new_builder( - schema=param, var_prefix=CMDArgBuildPrefix.Query, ref_args=ref_args + schema=param, var_prefix=var_prefix, ref_args=ref_args ) args.extend(builder.get_args()) return args @@ -130,11 +141,17 @@ class CMDHttpRequestHeader(CMDHttpRequestArgs): deserialize_from="clientRequestId", ) # specifies the header parameter to be used instead of `x-ms-client-request-id` - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): args = [] + if var_prefix: + if not var_prefix.endswith("$"): + var_prefix += '.' + var_prefix += CMDArgBuildPrefix.Header[1:] # Header + else: + var_prefix = CMDArgBuildPrefix.Header for param in self.params: builder = CMDArgBuilder.new_builder( - schema=param, var_prefix=CMDArgBuildPrefix.Header, ref_args=ref_args + schema=param, var_prefix=var_prefix, ref_args=ref_args ) args.extend(builder.get_args()) return args @@ -160,16 +177,16 @@ class CMDHttpRequest(Model): class Options: serialize_when_none = False - def generate_args(self, path, ref_args, has_subresource): + def generate_args(self, path, ref_args, has_subresource, var_prefix=None): args = [] if self.path: - args.extend(self.path.generate_args(path, ref_args, has_subresource)) + args.extend(self.path.generate_args(path, ref_args, has_subresource, var_prefix=var_prefix)) if self.query: - args.extend(self.query.generate_args(ref_args)) + args.extend(self.query.generate_args(ref_args, var_prefix=var_prefix)) if self.header: - args.extend(self.header.generate_args(ref_args)) + args.extend(self.header.generate_args(ref_args, var_prefix=var_prefix)) if self.body: - args.extend(self.body.generate_args(ref_args)) + args.extend(self.body.generate_args(ref_args, var_prefix=var_prefix)) return args def reformat(self, **kwargs): @@ -357,8 +374,9 @@ class CMDHttpAction(Model): request = ModelType(CMDHttpRequest) responses = ListType(ModelType(CMDHttpResponse)) - def generate_args(self, ref_args, has_subresource): - return self.request.generate_args(path=self.path, ref_args=ref_args, has_subresource=has_subresource) + def generate_args(self, ref_args, has_subresource, var_prefix=None): + return self.request.generate_args( + path=self.path, ref_args=ref_args, has_subresource=has_subresource, var_prefix=var_prefix) def reformat(self, **kwargs): if self.request: diff --git a/src/aaz_dev/command/model/configuration/_http_request_body.py b/src/aaz_dev/command/model/configuration/_http_request_body.py index 8fb9626a..74df7e5d 100644 --- a/src/aaz_dev/command/model/configuration/_http_request_body.py +++ b/src/aaz_dev/command/model/configuration/_http_request_body.py @@ -19,7 +19,7 @@ def _claim_polymorphic(cls, data): return False - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): raise NotImplementedError() def diff(self, old, level): @@ -37,8 +37,8 @@ class CMDHttpRequestJsonBody(CMDHttpRequestBody): json = ModelType(CMDRequestJson, required=True) - def generate_args(self, ref_args): - return self.json.generate_args(ref_args=ref_args) + def generate_args(self, ref_args, var_prefix=None): + return self.json.generate_args(ref_args=ref_args, var_prefix=var_prefix) def diff(self, old, level): if not isinstance(old, self.__class__): diff --git a/src/aaz_dev/command/model/configuration/_instance_create.py b/src/aaz_dev/command/model/configuration/_instance_create.py index b1f01f2e..34c3c5ac 100644 --- a/src/aaz_dev/command/model/configuration/_instance_create.py +++ b/src/aaz_dev/command/model/configuration/_instance_create.py @@ -26,7 +26,7 @@ def _claim_polymorphic(cls, data): return hasattr(data, cls.POLYMORPHIC_KEY) return False - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): raise NotImplementedError() def reformat(self, **kwargs): @@ -43,8 +43,10 @@ class CMDJsonInstanceCreateAction(CMDInstanceCreateAction): # properties as nodes json = ModelType(CMDRequestJson, required=True) - def generate_args(self, ref_args): - return self.json.generate_args(ref_args=ref_args, is_update_action=False) + def generate_args(self, ref_args, var_prefix=None): + return self.json.generate_args( + ref_args=ref_args, var_prefix=var_prefix, is_update_action=False + ) def reformat(self, **kwargs): self.json.reformat(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_instance_delete.py b/src/aaz_dev/command/model/configuration/_instance_delete.py index 83517596..edab8e50 100644 --- a/src/aaz_dev/command/model/configuration/_instance_delete.py +++ b/src/aaz_dev/command/model/configuration/_instance_delete.py @@ -26,7 +26,7 @@ def _claim_polymorphic(cls, data): return hasattr(data, cls.POLYMORPHIC_KEY) return False - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): raise NotImplementedError() def reformat(self, **kwargs): @@ -43,8 +43,8 @@ class CMDJsonInstanceDeleteAction(CMDInstanceDeleteAction): # # properties as nodes json = ModelType(CMDRequestJson, required=True) - def generate_args(self, ref_args): - return self.json.generate_args(ref_args) + def generate_args(self, ref_args, var_prefix=None): + return self.json.generate_args(ref_args, var_prefix=var_prefix) def reformat(self, **kwargs): self.json.reformat(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_instance_update.py b/src/aaz_dev/command/model/configuration/_instance_update.py index fbec8754..c9ef6a26 100644 --- a/src/aaz_dev/command/model/configuration/_instance_update.py +++ b/src/aaz_dev/command/model/configuration/_instance_update.py @@ -26,7 +26,7 @@ def _claim_polymorphic(cls, data): return hasattr(data, cls.POLYMORPHIC_KEY) return False - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): raise NotImplementedError() def reformat(self, **kwargs): @@ -43,8 +43,8 @@ class CMDJsonInstanceUpdateAction(CMDInstanceUpdateAction): # properties as nodes json = ModelType(CMDRequestJson, required=True) - def generate_args(self, ref_args): - return self.json.generate_args(ref_args=ref_args, is_update_action=True) + def generate_args(self, ref_args, var_prefix=None): + return self.json.generate_args(ref_args=ref_args, var_prefix=var_prefix, is_update_action=True) def reformat(self, **kwargs): self.json.reformat(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_operation.py b/src/aaz_dev/command/model/configuration/_operation.py index 7fb09f6b..d753e80d 100644 --- a/src/aaz_dev/command/model/configuration/_operation.py +++ b/src/aaz_dev/command/model/configuration/_operation.py @@ -29,7 +29,7 @@ def _claim_polymorphic(cls, data): return hasattr(data, cls.POLYMORPHIC_KEY) return False - def generate_args(self, ref_args, has_subresource): + def generate_args(self, ref_args, has_subresource, var_prefix=None): raise NotImplementedError() def reformat(self, **kwargs): @@ -86,8 +86,8 @@ class CMDHttpOperation(CMDOperation): # properties as nodes http = ModelType(CMDHttpAction, required=True) - def generate_args(self, ref_args, has_subresource): - return self.http.generate_args(ref_args=ref_args, has_subresource=has_subresource) + def generate_args(self, ref_args, has_subresource, var_prefix=None): + return self.http.generate_args(ref_args=ref_args, has_subresource=has_subresource, var_prefix=var_prefix) def reformat(self, **kwargs): self.http.reformat(**kwargs) @@ -131,8 +131,8 @@ class CMDInstanceCreateOperation(CMDOperation): deserialize_from="instanceCreate" ) - def generate_args(self, ref_args, has_subresource): - return self.instance_create.generate_args(ref_args=ref_args) + def generate_args(self, ref_args, has_subresource, var_prefix=None): + return self.instance_create.generate_args(ref_args=ref_args, var_prefix=var_prefix) def reformat(self, **kwargs): self.instance_create.reformat(**kwargs) @@ -153,8 +153,8 @@ class CMDInstanceUpdateOperation(CMDOperation): deserialize_from="instanceUpdate" ) - def generate_args(self, ref_args, has_subresource): - return self.instance_update.generate_args(ref_args=ref_args) + def generate_args(self, ref_args, has_subresource, var_prefix=None): + return self.instance_update.generate_args(ref_args=ref_args, var_prefix=var_prefix) def reformat(self, **kwargs): self.instance_update.reformat(**kwargs) @@ -174,8 +174,8 @@ class CMDInstanceDeleteOperation(CMDOperation): deserialize_from="instanceDelete" ) - def generate_args(self, ref_args, has_subresource): - return self.instance_delete.generate_args(ref_args=ref_args) + def generate_args(self, ref_args, has_subresource, var_prefix=None): + return self.instance_delete.generate_args(ref_args=ref_args, var_prefix=var_prefix) def reformat(self, **kwargs): self.instance_delete.reformat(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_selector_index.py b/src/aaz_dev/command/model/configuration/_selector_index.py index bf11c67e..b5c434f2 100644 --- a/src/aaz_dev/command/model/configuration/_selector_index.py +++ b/src/aaz_dev/command/model/configuration/_selector_index.py @@ -7,7 +7,7 @@ from schematics.types import StringType, ListType, ModelType, PolyModelType from schematics.types.serializable import serializable -from ._schema import CMDSchemaField +from ._schema import CMDSchemaField, CMDStringSchemaBase, CMDStringSchema from ._arg_builder import CMDArgBuilder from ._utils import CMDDiffLevelEnum @@ -363,6 +363,32 @@ def generate_args(self, ref_args, var_prefix): return self._generate_args_base(ref_args, var_prefix) +# simple type index, used to select base types such as string, number, boolean, etc. +class CMDSimpleIndexBase(CMDSelectorIndexBase): + TYPE_VALUE = "simple" + + supported_schema_types = (CMDStringSchemaBase,) + + def _generate_args_base(self, ref_args, var_prefix): + return [] + + def generate_args(self, ref_args, var_prefix): + return self._generate_args_base(ref_args, var_prefix) + + def reformat(self, **kwargs): + pass + + def _diff_base(self, old, level, diff): + return diff + + +class CMDSimpleIndex(CMDSimpleIndexBase, CMDSelectorIndex): + + supported_schema_types = (CMDStringSchema,) + + pass + + def _diff_identifiers(self_identifiers, old_identifiers, level): identifiers_diff = {} if level >= CMDDiffLevelEnum.BreakingChange: diff --git a/src/aaz_dev/command/model/configuration/_subresource_selector.py b/src/aaz_dev/command/model/configuration/_subresource_selector.py index 5663b433..7eba21eb 100644 --- a/src/aaz_dev/command/model/configuration/_subresource_selector.py +++ b/src/aaz_dev/command/model/configuration/_subresource_selector.py @@ -7,15 +7,14 @@ from ._fields import CMDVariantField from ._selector_index import CMDSelectorIndexField -from ._utils import CMDDiffLevelEnum +from ._utils import CMDDiffLevelEnum, CMDBuildInVariants class CMDSubresourceSelector(Model): POLYMORPHIC_KEY = None - DEFAULT_VARIANT = "$Subresource" # properties as tags - var = CMDVariantField(required=True, default=DEFAULT_VARIANT) + var = CMDVariantField(required=True, default=CMDBuildInVariants.Subresource) ref = CMDVariantField(required=True) class Options: @@ -32,7 +31,7 @@ def _claim_polymorphic(cls, data): return hasattr(data, cls.POLYMORPHIC_KEY) return False - def generate_args(self, ref_args): + def generate_args(self, ref_args, var_prefix=None): raise NotImplementedError() def reformat(self, **kwargs): @@ -119,8 +118,9 @@ class CMDJsonSubresourceSelector(CMDSubresourceSelector): # properties as nodes json = CMDSelectorIndexField(required=True) - def generate_args(self, ref_args): - return self.json.generate_args(ref_args, "$") + def generate_args(self, ref_args, var_prefix=None): + var_prefix = var_prefix or "$" + return self.json.generate_args(ref_args, var_prefix) def reformat(self, **kwargs): self.json.reformat(**kwargs) diff --git a/src/aaz_dev/command/model/configuration/_utils.py b/src/aaz_dev/command/model/configuration/_utils.py index d0e87cfe..cfce5a68 100644 --- a/src/aaz_dev/command/model/configuration/_utils.py +++ b/src/aaz_dev/command/model/configuration/_utils.py @@ -10,7 +10,17 @@ class CMDArgBuildPrefix: Query = '$Query' Header = '$Header' Path = '$Path' - ClientEndpoint = '$Client.Endpoint' + ClientEndpoint = '$Client.Endpoint' # It's used for client config endpoints related args DEFAULT_CONFIRMATION_PROMPT = "Are you sure you want to perform this operation?" + + +class CMDBuildInVariants: + + Instance = "$Instance" + # It's used in client config which endpoints is by http operation + EndpointInstance = "$EndpointInstance" + + Subresource = "$Subresource" + Endpoint = "$Endpoint" diff --git a/src/aaz_dev/command/tests/api_tests/test_editor.py b/src/aaz_dev/command/tests/api_tests/test_editor.py index f9c99e30..d1376589 100644 --- a/src/aaz_dev/command/tests/api_tests/test_editor.py +++ b/src/aaz_dev/command/tests/api_tests/test_editor.py @@ -2233,7 +2233,7 @@ def test_dataplane_monitor_metrics(self, ws_name): rv = c.get(f"{ws_url}/ClientConfig/Arguments/$Client.Endpoint.region") self.assertTrue(rv.status_code == 200) client_arg = rv.get_json() - self.assertEqual(client_arg, {'options': ['region'], 'required': True, 'type': 'string', + self.assertEqual(client_arg, {'group': 'Client', 'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'}) # add resources @@ -2280,3 +2280,205 @@ def test_dataplane_monitor_metrics(self, ws_name): rv = c.post(f"{ws_url}/Generate") self.assertTrue(rv.status_code == 200) + + @workspace_name("test_mgmt_attestation") + def test_mgmt_attestation(self, ws_name): + module = "attestation" + resource_provider = "Microsoft.Attestation" + api_version = '2021-06-01-preview' + + with self.app.test_client() as c: + rv = c.post(f"/AAZ/Editor/Workspaces", json={ + "name": ws_name, + "plane": PlaneEnum.Mgmt, + "modNames": module, + "resourceProvider": resource_provider + }) + self.assertTrue(rv.status_code == 200) + ws = rv.get_json() + self.assertEqual(ws['plane'], PlaneEnum.Mgmt) + self.assertEqual(ws['resourceProvider'], resource_provider) + self.assertEqual(ws['modNames'], module) + ws_url = ws['url'] + + # add resources + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger", json={ + 'module': module, + 'version': api_version, + 'resources': [ + {'id': swagger_resource_path_to_resource_id('/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}')}, + {'id': swagger_resource_path_to_resource_id('/subscriptions/{subscriptionId}/providers/Microsoft.Attestation/attestationProviders')}, + {'id': swagger_resource_path_to_resource_id('/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders')}, + ] + }) + self.assertTrue(rv.status_code == 200) + + command_tree = rv.get_json() + + # modify command tree + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/attestation-provider/Rename", json={ + "name": "attestation provider" + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/provider", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.get(f"{ws_url}/CommandTree/Nodes/aaz") + self.assertTrue(rv.status_code == 200) + command_tree = rv.get_json() + + rv = c.post(f"{ws_url}/Generate") + self.assertTrue(rv.status_code == 200) + + @workspace_name("test_dataplane_attestation") + def test_dataplane_attestation(self, ws_name): + module = "attestation" + resource_provider = "Microsoft.Attestation" + api_version = '2022-09-01-preview' + + with self.app.test_client() as c: + rv = c.post(f"/AAZ/Editor/Workspaces", json={ + "name": ws_name, + "plane": PlaneEnum._Data, + "modNames": module, + "resourceProvider": resource_provider + }) + self.assertTrue(rv.status_code == 200) + ws = rv.get_json() + self.assertEqual(ws['plane'], PlaneEnum.Data(resource_provider)) + self.assertEqual(ws['resourceProvider'], resource_provider) + self.assertEqual(ws['modNames'], module) + ws_url = ws['url'] + + # add client configuration + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id('/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01", + "subresource": "properties.attestUri", + }, + }) + self.assertTrue(rv.status_code == 200) + rv = c.get(f"{ws_url}/ClientConfig") + self.assertTrue(rv.status_code == 200) + client_config = rv.get_json() + self.assertEqual(client_config['endpoints']['type'], 'http-operation') + self.assertEqual(client_config['endpoints']['selector'], {'json': {'name': 'response', 'prop': {'name': 'properties.attestUri', 'type': 'simple'}, 'type': 'object'}, 'ref': '$EndpointInstance', 'var': '$Endpoint'}) + self.assertEqual(client_config['endpoints']['resource']['id'], swagger_resource_path_to_resource_id('/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}')) + self.assertEqual(client_config['endpoints']['resource']['version'], '2021-06-01') + self.assertEqual(client_config['endpoints']['resource']['subresource'], 'properties.attestUri') + self.assertEqual(client_config['argGroup'], { + 'name': 'Client', + 'args': [ + { + 'var': '$Client.Endpoint.Path.providerName', + 'options': ['provider-name'], + 'required': True, + 'type': 'string', + 'group': 'Client', + 'idPart': 'name', + 'help': {'short': 'Name of the attestation provider.'}, + }, + { + 'var': '$Client.Endpoint.Path.resourceGroupName', + 'options': ['g', 'resource-group'], + 'required': True, + 'type': 'ResourceGroupName', + 'group': 'Client', + 'idPart': 'resource_group', + }, + { + 'var': '$Client.Endpoint.Path.subscriptionId', + 'options': ['subscription'], + 'required': True, + 'type': 'SubscriptionId', + 'group': 'Client', + 'idPart': 'subscription', + } + ] + }) + + # add resources + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger", json={ + 'module': module, + 'version': api_version, + 'resources': [ + {'id': swagger_resource_path_to_resource_id('/policies/{attestationType}'), 'options': {'update_by': 'None'}}, + {'id': swagger_resource_path_to_resource_id('/policies/{attestationType}:reset')}, + {'id': swagger_resource_path_to_resource_id('/certificates')}, + {'id': swagger_resource_path_to_resource_id('/certificates:add')}, + {'id': swagger_resource_path_to_resource_id('/certificates:remove')}, + ] + }) + self.assertTrue(rv.status_code == 200) + + command_tree = rv.get_json() + + # modify command tree + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/policy/Leaves/create/Rename", json={ + "name": "attestation policy set" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/certificate/Leaves/show/Rename", json={ + "name": "attestation signer list" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/Leaves/certificatesadd/Rename", json={ + "name": "attestation signer add" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/Leaves/certificatesremove/Rename", json={ + "name": "attestation signer remove" + }) + self.assertTrue(rv.status_code == 200) + + rv = c.delete(f"{ws_url}/CommandTree/Nodes/aaz/attestation/certificate") + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/signer", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/policy", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.get(f"{ws_url}/CommandTree/Nodes/aaz") + self.assertTrue(rv.status_code == 200) + command_tree = rv.get_json() + + rv = c.post(f"{ws_url}/Generate") + self.assertTrue(rv.status_code == 200) + diff --git a/src/aaz_dev/command/tests/configuration_tests/test_xml.py b/src/aaz_dev/command/tests/configuration_tests/test_xml.py index dfef559e..1ed5ef2b 100644 --- a/src/aaz_dev/command/tests/configuration_tests/test_xml.py +++ b/src/aaz_dev/command/tests/configuration_tests/test_xml.py @@ -1,6 +1,6 @@ from tempfile import TemporaryFile -from command.model.configuration import CMDConfiguration, XMLSerializer +from command.model.configuration import CMDConfiguration, XMLSerializer, CMDBuildInVariants from swagger.controller.command_generator import CommandGenerator from swagger.controller.specs_manager import SwaggerSpecsManager from swagger.tests.common import SwaggerSpecsTestCase @@ -23,7 +23,8 @@ def test_virtual_network_e2e(self): resource = specs_module_manager.get_resource_in_version(resource_id=resource_id, version="2021-05-01") generator.load_resources([resource]) - command_group = generator.create_draft_command_group(resource) + command_group = generator.create_draft_command_group( + resource, instance_var=CMDBuildInVariants.Instance) cfg = CMDConfiguration({"resources": [resource.to_cmd()], "commandGroups": [command_group]}) with TemporaryFile("w+t", encoding="utf-8") as fp: @@ -43,7 +44,8 @@ def test_all_mgmt_modules_coverage(self): for v, resource in r_version_map.items(): try: generator.load_resources([resource]) - command_group = generator.create_draft_command_group(resource) + command_group = generator.create_draft_command_group( + resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) diff --git a/src/aaz_dev/swagger/controller/command_generator.py b/src/aaz_dev/swagger/controller/command_generator.py index 1b29a805..b7175e67 100644 --- a/src/aaz_dev/swagger/controller/command_generator.py +++ b/src/aaz_dev/swagger/controller/command_generator.py @@ -19,11 +19,6 @@ logger = logging.getLogger('backend') -class BuildInVariants: - - Instance = "$Instance" - - class CommandGenerator: _inflect_engine = inflect.engine() @@ -36,6 +31,7 @@ def load_resources(self, resources): self.loader.link_swaggers() def create_draft_command_group(self, resource, + instance_var, update_by=None, methods=('get', 'delete', 'put', 'post', 'head', 'patch'), **kwargs): @@ -52,32 +48,32 @@ def create_draft_command_group(self, resource, if path_item.get is not None and 'get' in methods: cmd_builder = CMDBuilder(path=resource.path, method='get', mutability=MutabilityEnum.Read, parameterized_host=swagger.x_ms_parameterized_host) - show_or_list_command = self.generate_command(path_item, resource, cmd_builder) + show_or_list_command = self.generate_command(path_item, resource, instance_var, cmd_builder) command_group.commands.append(show_or_list_command) if path_item.delete is not None and 'delete' in methods: cmd_builder = CMDBuilder(path=resource.path, method='delete', mutability=MutabilityEnum.Create, parameterized_host=swagger.x_ms_parameterized_host) - delete_command = self.generate_command(path_item, resource, cmd_builder) + delete_command = self.generate_command(path_item, resource, instance_var, cmd_builder) delete_command.confirmation = DEFAULT_CONFIRMATION_PROMPT # add confirmation for delete command by default command_group.commands.append(delete_command) if path_item.put is not None and 'put' in methods: cmd_builder = CMDBuilder(path=resource.path, method='put', mutability=MutabilityEnum.Create, parameterized_host=swagger.x_ms_parameterized_host) - create_command = self.generate_command(path_item, resource, cmd_builder) + create_command = self.generate_command(path_item, resource, instance_var, cmd_builder) command_group.commands.append(create_command) if path_item.post is not None and 'post' in methods: cmd_builder = CMDBuilder(path=resource.path, method='post', mutability=MutabilityEnum.Create, parameterized_host=swagger.x_ms_parameterized_host) - action_command = self.generate_command(path_item, resource, cmd_builder) + action_command = self.generate_command(path_item, resource, instance_var, cmd_builder) command_group.commands.append(action_command) if path_item.head is not None and 'head' in methods: cmd_builder = CMDBuilder(path=resource.path, method='head', mutability=MutabilityEnum.Read, parameterized_host=swagger.x_ms_parameterized_host) - head_command = self.generate_command(path_item, resource, cmd_builder) + head_command = self.generate_command(path_item, resource, instance_var, cmd_builder) command_group.commands.append(head_command) # update command @@ -87,11 +83,11 @@ def create_draft_command_group(self, resource, if path_item.patch is not None and 'patch' in methods: cmd_builder = CMDBuilder(path=resource.path, method='patch', mutability=MutabilityEnum.Update, parameterized_host=swagger.x_ms_parameterized_host) - update_by_patch_command = self.generate_command(path_item, resource, cmd_builder) + update_by_patch_command = self.generate_command(path_item, resource, instance_var, cmd_builder) if path_item.get is not None and path_item.put is not None and 'get' in methods and 'put' in methods: cmd_builder = CMDBuilder(path=resource.path, parameterized_host=swagger.x_ms_parameterized_host) - update_by_generic_command = self.generate_generic_update_command(path_item, resource, cmd_builder) + update_by_generic_command = self.generate_generic_update_command(path_item, resource, instance_var, cmd_builder) # generic update command first, patch update command after that if update_by_generic_command: command_group.commands.append(update_by_generic_command) @@ -105,7 +101,7 @@ def create_draft_command_group(self, resource, raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: '{resource}': 'get' or 'put' not in methods: '{methods}'") cmd_builder = CMDBuilder(path=resource.path, parameterized_host=swagger.x_ms_parameterized_host) - generic_update_command = self.generate_generic_update_command(path_item, resource, cmd_builder) + generic_update_command = self.generate_generic_update_command(path_item, resource, instance_var, cmd_builder) if generic_update_command is None: raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: failed to generate generic update: '{resource}'") command_group.commands.append(generic_update_command) @@ -118,7 +114,7 @@ def create_draft_command_group(self, resource, raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: '{resource}': 'patch' not in methods: '{methods}'") cmd_builder = CMDBuilder(path=resource.path, method='patch', mutability=MutabilityEnum.Update, parameterized_host=swagger.x_ms_parameterized_host) - patch_update_command = self.generate_command(path_item, resource, cmd_builder) + patch_update_command = self.generate_command(path_item, resource, instance_var, cmd_builder) command_group.commands.append(patch_update_command) # elif update_by == 'GenericAndPatch': # # TODO: add support for generic and patch merge @@ -128,12 +124,12 @@ def create_draft_command_group(self, resource, # raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: '{resource}': 'get' or 'put' or 'patch' not in methods: '{methods}'") # cmd_builder = CMDBuilder(path=resource.path, # parameterized_host=swagger.x_ms_parameterized_host) - # generic_update_command = self.generate_generic_update_command(path_item, resource, cmd_builder) + # generic_update_command = self.generate_generic_update_command(path_item, resource, instance_var, cmd_builder) # if generic_update_command is None: # raise exceptions.InvalidAPIUsage(f"Invalid update_by resource: failed to generate generic update: '{resource}'") # cmd_builder = CMDBuilder(path=resource.path, method='patch', mutability=MutabilityEnum.Update, # parameterized_host=swagger.x_ms_parameterized_host) - # patch_update_command = self.generate_command(path_item, resource, cmd_builder) + # patch_update_command = self.generate_command(path_item, resource, instance_var, cmd_builder) # generic_and_patch_update_command = self._merge_update_commands( # patch_command=patch_update_command, generic_command=generic_update_command # ) @@ -157,14 +153,14 @@ def create_draft_command_group(self, resource, def generate_command_version(resource): return resource.version - def generate_command(self, path_item, resource, cmd_builder): + def generate_command(self, path_item, resource, instance_var, cmd_builder): command = CMDCommand() command.version = self.generate_command_version(resource) command.resources = [ resource.to_cmd() ] - op = self._generate_operation(cmd_builder, path_item) + op = self._generate_operation(cmd_builder, path_item, instance_var) cmd_builder.apply_cls_definitions(op) assert isinstance(op, CMDHttpOperation) @@ -183,7 +179,7 @@ def generate_command(self, path_item, resource, cmd_builder): return command - def generate_generic_update_command(self, path_item, resource, cmd_builder): + def generate_generic_update_command(self, path_item, resource, instance_var, cmd_builder): command = CMDCommand() command.version = self.generate_command_version(resource) command.resources = [ @@ -192,8 +188,10 @@ def generate_generic_update_command(self, path_item, resource, cmd_builder): assert path_item.get is not None assert path_item.put is not None - get_op = self._generate_operation(cmd_builder, path_item, method='get', mutability=MutabilityEnum.Read) - put_op = self._generate_operation(cmd_builder, path_item, method='put', mutability=MutabilityEnum.Update) + get_op = self._generate_operation( + cmd_builder, path_item, instance_var, method='get', mutability=MutabilityEnum.Read) + put_op = self._generate_operation( + cmd_builder, path_item, instance_var, method='put', mutability=MutabilityEnum.Update) cmd_builder.apply_cls_definitions(get_op, put_op) @@ -214,7 +212,7 @@ def generate_generic_update_command(self, path_item, resource, cmd_builder): self._filter_generic_update_parameters(get_op, put_op) command.description = put_op.description - json_update_op = self._generate_instance_update_operation(put_op) + json_update_op = self._generate_instance_update_operation(put_op, instance_var) command.operations = [ get_op, json_update_op, @@ -232,7 +230,7 @@ def generate_generic_update_command(self, path_item, resource, cmd_builder): return command @staticmethod - def _generate_operation(cmd_builder, path_item, **kwargs): + def _generate_operation(cmd_builder, path_item, instance_var, **kwargs): op = cmd_builder(path_item, **kwargs) assert isinstance(op, CMDHttpOperation) @@ -267,7 +265,7 @@ def _generate_operation(cmd_builder, path_item, **kwargs): if resp.body is None: continue if isinstance(resp.body, CMDHttpResponseJsonBody): - resp.body.json.var = BuildInVariants.Instance + resp.body.json.var = instance_var if not error_format: # TODO: refactor the following line to support data plane command generation. @@ -433,20 +431,20 @@ def _generate_command_name(self, path_item, resource, method, output): # For update @staticmethod - def _generate_instance_update_operation(put_op): + def _generate_instance_update_operation(put_op, instance_var): json_update_op = CMDInstanceUpdateOperation() json_update_op.instance_update = CMDJsonInstanceUpdateAction() - json_update_op.instance_update.ref = BuildInVariants.Instance + json_update_op.instance_update.ref = instance_var json_update_op.instance_update.json = CMDRequestJson() json_update_op.instance_update.json.schema = put_op.http.request.body.json.schema - put_op.http.request.body.json.ref = BuildInVariants.Instance + put_op.http.request.body.json.ref = instance_var put_op.http.request.body.json.schema = None return json_update_op @staticmethod def _filter_generic_update_parameters(get_op, put_op): - """Get operation may contains useless query or header parameters for update, ignore them""" + """Get operation may contain useless query or header parameters for update, ignore them""" get_request = get_op.http.request put_request = put_op.http.request diff --git a/src/aaz_dev/swagger/controller/specs_manager.py b/src/aaz_dev/swagger/controller/specs_manager.py index 64fdc230..4bf9fd6f 100644 --- a/src/aaz_dev/swagger/controller/specs_manager.py +++ b/src/aaz_dev/swagger/controller/specs_manager.py @@ -174,3 +174,8 @@ def get_module_manager(self, plane, mod_names, without_catch=False) -> SwaggerSp self._module_managers_cache[key] = SwaggerSpecsModuleManager(plane, module) return self._module_managers_cache[key] + + def get_swagger_resource(self, plane, mod_names, resource_id, version): + return self.get_module_manager( + plane=plane, mod_names=mod_names + ).get_resource_in_version(resource_id, version) diff --git a/src/aaz_dev/swagger/tests/controller_tests/test_command_generator.py b/src/aaz_dev/swagger/tests/controller_tests/test_command_generator.py index 6efa81d5..513ddb3a 100644 --- a/src/aaz_dev/swagger/tests/controller_tests/test_command_generator.py +++ b/src/aaz_dev/swagger/tests/controller_tests/test_command_generator.py @@ -2,6 +2,8 @@ from swagger.controller.command_generator import CommandGenerator from swagger.model.specs._utils import get_url_path_valid_parts from swagger.utils import exceptions +from command.model.configuration import CMDBuildInVariants + MUTE_ERROR_MESSAGES = ( "type is not supported", @@ -27,7 +29,7 @@ def test_monitor_control_service(self): continue try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -51,7 +53,7 @@ def test_data_factory_integration_runtimes(self): continue try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -75,7 +77,7 @@ def test_data_factory(self): continue try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -96,7 +98,7 @@ def test_recovery_services(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -117,7 +119,7 @@ def test_storagecache(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -138,7 +140,7 @@ def test_databox(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -160,7 +162,7 @@ def test_network(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -188,7 +190,7 @@ def test_mgmt_modules(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) @@ -213,7 +215,7 @@ def test_data_plane_modules(self): for v, resource in version_map.items(): try: generator.load_resources([resource]) - generator.create_draft_command_group(resource) + generator.create_draft_command_group(resource, instance_var=CMDBuildInVariants.Instance) except exceptions.InvalidSwaggerValueError as err: if err.msg not in MUTE_ERROR_MESSAGES: print(err) diff --git a/src/aaz_dev/swagger/tests/schema_tests/test_schema.py b/src/aaz_dev/swagger/tests/schema_tests/test_schema.py index 0365d8d7..e317c7b2 100644 --- a/src/aaz_dev/swagger/tests/schema_tests/test_schema.py +++ b/src/aaz_dev/swagger/tests/schema_tests/test_schema.py @@ -231,6 +231,7 @@ def test_XmsParameterizedHost(self): def test_lro_final_state_schema(self): from functools import reduce from swagger.controller.command_generator import CommandGenerator + from command.model.configuration import CMDBuildInVariants rp = next(self.get_mgmt_plane_resource_providers( module_filter=lambda m: m.name == "dnsresolver", @@ -244,7 +245,8 @@ def test_lro_final_state_schema(self): generator = CommandGenerator() generator.load_resources([resource]) - command_group = generator.create_draft_command_group(resource, methods={"put"}) # only modify PUT operation + command_group = generator.create_draft_command_group( + resource, instance_var=CMDBuildInVariants.Instance, methods={"put"}) # only modify PUT operation responses = command_group.commands[0].operations[0].http.responses status_codes = reduce(lambda x, y: x + y, [r.status_codes for r in responses]) From 85dab5c2c09fb795ed0213dd9aac9bcd0b565c7f Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Wed, 22 Nov 2023 12:50:26 +0800 Subject: [PATCH 4/9] Support generate clients with dynamic endpoints --- .../controller/az_atomic_profile_builder.py | 5 +- .../cli/controller/az_client_generator.py | 82 ++- src/aaz_dev/cli/controller/az_command_ctx.py | 109 ++++ .../cli/controller/az_command_generator.py | 146 +----- .../cli/controller/az_selector_generator.py | 10 +- src/aaz_dev/cli/model/atomic/_client.py | 5 + .../cli/templates/aaz/profile/_clients.py.j2 | 31 +- src/aaz_dev/cli/templates/macros.j2 | 466 ++++++++++++++++++ src/aaz_dev/cli/tests/api_tests/test_az.py | 111 +++++ src/aaz_dev/cli/tests/common.py | 178 ++++++- 10 files changed, 1009 insertions(+), 134 deletions(-) create mode 100644 src/aaz_dev/cli/controller/az_command_ctx.py create mode 100644 src/aaz_dev/cli/templates/macros.j2 diff --git a/src/aaz_dev/cli/controller/az_atomic_profile_builder.py b/src/aaz_dev/cli/controller/az_atomic_profile_builder.py index fc8bce97..1264b1de 100644 --- a/src/aaz_dev/cli/controller/az_atomic_profile_builder.py +++ b/src/aaz_dev/cli/controller/az_atomic_profile_builder.py @@ -166,14 +166,15 @@ def _build_client_from_aaz(self, plane): if plane in PlaneEnum._config: # use the clients registered in azure/cli/core/aaz/_client.py client.registered_name = client.name + client.cls_name = client.name else: # generate client based on client config cfg_reader = self._aaz_spec_manager.load_client_cfg_reader(plane) assert cfg_reader, "Missing Client config for '" + plane + "' plane." client.cfg = cfg_reader.cfg scope = PlaneEnum.get_data_plane_scope(plane) or plane - client.registered_name = (to_camel_case(f"AAZ {scope.replace('.', ' ')} {client.name}") + - f'_{to_snake_case(self._mod_name)}') # for example: AAZAzureCodesigningDataPlaneClient_network + client.cls_name = to_camel_case(f"AAZ {scope.replace('.', ' ')} {client.name}") + client.registered_name = f'{client.cls_name}_{to_snake_case(self._mod_name)}' # for example: AAZAzureCodesigningDataPlaneClient_network return client @classmethod diff --git a/src/aaz_dev/cli/controller/az_client_generator.py b/src/aaz_dev/cli/controller/az_client_generator.py index 5de3f661..680d20ab 100644 --- a/src/aaz_dev/cli/controller/az_client_generator.py +++ b/src/aaz_dev/cli/controller/az_client_generator.py @@ -1,6 +1,15 @@ from utils import exceptions import logging -from cli.model.atomic import CLIAtomicCommand, CLIAtomicClient + +from cli.model.atomic import CLIAtomicClient +from command.model.configuration import CMDClientEndpointsByHttpOperation, CMDClientEndpointsByTemplate, CMDJsonSubresourceSelector +from .az_command_ctx import AzCommandCtx +from .az_command_generator import AzCommandGenerator +from .az_arg_group_generator import AzArgGroupGenerator +from .az_selector_generator import AzJsonSelectorGenerator +from .az_operation_generator import AzHttpOperationGenerator +from utils.plane import PlaneEnum +from utils.case import to_camel_case logger = logging.getLogger('backend') @@ -23,15 +32,80 @@ class AzClientGenerator: def __init__(self, client): self._client = client + self.cmd_ctx = AzCommandCtx() + if self._client.cfg.arg_group: + # Register client arguments in _cmd_ctx + AzArgGroupGenerator(AzCommandGenerator.ARGS_SCHEMA_NAME, self.cmd_ctx, self._client.cfg.arg_group) + if isinstance(self._client.cfg.endpoints, CMDClientEndpointsByTemplate): + self.endpoints = AzClientEndpointsByTemplateGenerator(self._client.cfg.endpoints) + elif isinstance(self._client.cfg.endpoints, CMDClientEndpointsByHttpOperation): + self.endpoints = AzClientEndpointsByHttpOperationGenerator(self._client.cfg.endpoints, self.cmd_ctx) + else: + raise NotImplementedError() @property def registered_name(self): return self._client.registered_name - def iter_hosts(self): - for template in self._client.cfg.endpoints.templates: - yield template.cloud, template.template + @property + def cls_name(self): + return self._client.cls_name @property def aad_scopes(self): return self._client.cfg.auth.aad.scopes + + @property + def helper_cls_name(self): + return f'_{self.cls_name}Helper' + + def get_arg_clses(self): + return sorted(self.cmd_ctx.arg_clses.values(), key=lambda a: a.name) + + def get_update_clses(self): + return sorted(self.cmd_ctx.update_clses.values(), key=lambda s: s.name) + + def get_response_clses(self): + return sorted(self.cmd_ctx.response_clses.values(), key=lambda s: s.name) + + +class AzClientEndpointsByTemplateGenerator: + + def __init__(self, endpoints): + assert isinstance(endpoints, CMDClientEndpointsByTemplate) + self._endpoints = endpoints + + @property + def type(self): + return self._endpoints.type + + def iter_hosts(self): + for template in self._endpoints.templates: + yield template.cloud, template.template + + +class AzClientEndpointsByHttpOperationGenerator: + + def __init__(self, endpoints, cmd_ctx): + assert isinstance(endpoints, CMDClientEndpointsByHttpOperation) + self._endpoints = endpoints + self._cmd_ctx = cmd_ctx + if isinstance(endpoints.selector, CMDJsonSubresourceSelector): + self.selector = AzJsonSelectorGenerator(self._cmd_ctx, endpoints.selector) + else: + raise NotImplementedError() + op_cls_name = to_camel_case(endpoints.operation.operation_id) + self.operation = AzHttpOperationGenerator( + op_cls_name, + self._cmd_ctx, + endpoints.operation, + client_endpoints=None, + ) + # Only arm endpoints is allowed + assert endpoints.resource.plane == PlaneEnum.Mgmt + self.client_name = PlaneEnum.http_client(endpoints.resource.plane) + + @property + def type(self): + return self._endpoints.type + diff --git a/src/aaz_dev/cli/controller/az_command_ctx.py b/src/aaz_dev/cli/controller/az_command_ctx.py new file mode 100644 index 00000000..464e5437 --- /dev/null +++ b/src/aaz_dev/cli/controller/az_command_ctx.py @@ -0,0 +1,109 @@ +import logging + +from command.model.configuration import CMDResourceGroupNameArg +from utils import exceptions +from utils.case import to_snake_case +from .az_arg_group_generator import AzArgClsGenerator +from .az_operation_generator import AzRequestClsGenerator, AzResponseClsGenerator + +logger = logging.getLogger('backend') + + +class AzCommandCtx: + + def __init__(self): + self._cls_arg_maps = {} + self._ctx_arg_map = {} + self._selectors = {} + self.rg_arg_var = None + + self.arg_clses = {} + self.update_clses = {} + self.response_clses = {} + self.support_id_part = True + + def set_argument_cls(self, arg): + cls_name = arg.cls + self._cls_arg_maps[f"@{cls_name}"] = {} + assert cls_name not in self.arg_clses, f"Argument class {cls_name} is defined more than one place" + self.arg_clses[cls_name] = AzArgClsGenerator(cls_name, self, arg) + + def set_argument(self, keys, arg, ctx_namespace='self.ctx.args'): + var_name = arg.var + hide = arg.hide + if var_name.startswith('@'): + map_name = var_name.replace('[', '.[').replace('{', '.{').split('.', maxsplit=1)[0] + if map_name != keys[0]: + raise exceptions.VerificationError( + "Invalid argument var", + details=f"argument var '{var_name}' does not start with '{keys[0]}'" + ) + if map_name not in self._cls_arg_maps: + self._cls_arg_maps[map_name] = {} + self._cls_arg_maps[map_name][var_name] = ( + '.'.join(keys).replace('.[', '[').replace('.{', '{'), + hide + ) + else: + self._ctx_arg_map[var_name] = ( + '.'.join([ctx_namespace, *keys]).replace('.[', '[').replace('.{', '{'), + hide + ) + + if isinstance(arg, CMDResourceGroupNameArg): + assert self.rg_arg_var is None, "Resource Group Argument defined twice" + self.rg_arg_var = arg.var + + def get_argument(self, var_name): + if var_name.startswith('@'): + map_name = var_name.replace('[', '.[').replace('{', '.{').split('.', maxsplit=1)[0] + if map_name not in self._cls_arg_maps: + raise exceptions.VerificationError( + "Invalid argument var", + details=f"argument var '{var_name}' has unregistered class '{map_name}'." + ) + if var_name not in self._cls_arg_maps[map_name]: + raise exceptions.VerificationError( + "Invalid argument var", + details=f"argument var '{var_name}' does not find." + ) + return self._cls_arg_maps[map_name][var_name] + else: + if var_name not in self._ctx_arg_map: + raise exceptions.VerificationError( + "Invalid argument var", + details=f"argument var '{var_name}' does not find." + ) + return self._ctx_arg_map[var_name] + + def get_variant(self, variant, name_only=False): + if variant.startswith('$'): + variant = variant[1:] + variant = to_snake_case(variant) + if name_only: + return variant + + is_selector = variant in self._selectors + + if is_selector: + return f'self.ctx.selectors.{variant}', is_selector + else: + return f'self.ctx.vars.{variant}', is_selector + + def set_update_cls(self, schema): + cls_name = schema.cls + assert cls_name not in self.update_clses, f"Schema cls '{cls_name}', is defined more than once" + self.update_clses[cls_name] = AzRequestClsGenerator(self, cls_name, schema) + + def set_response_cls(self, schema): + cls_name = schema.cls + assert cls_name not in self.response_clses, f"Schema cls '{cls_name}', is defined more than once" + self.response_clses[cls_name] = AzResponseClsGenerator(self, cls_name, schema) + + def set_selector(self, selector): + self._selectors[self.get_variant(selector.var, name_only=True)] = selector + + def render_arg_resource_id_template(self, template): + # TODO: fill blank placeholders as much as possible + + return template diff --git a/src/aaz_dev/cli/controller/az_command_generator.py b/src/aaz_dev/cli/controller/az_command_generator.py index 9b78d00e..bee06407 100644 --- a/src/aaz_dev/cli/controller/az_command_generator.py +++ b/src/aaz_dev/cli/controller/az_command_generator.py @@ -1,125 +1,23 @@ +import logging + from cli.model.atomic import CLIAtomicCommand, CLIAtomicClient from command.model.configuration import CMDCommand, CMDHttpOperation, CMDCondition, CMDConditionAndOperator, \ CMDConditionOrOperator, CMDConditionNotOperator, CMDConditionHasValueOperator, CMDInstanceUpdateOperation, \ - CMDJsonInstanceUpdateAction, CMDResourceGroupNameArg, CMDJsonSubresourceSelector, CMDInstanceCreateOperation, \ - CMDInstanceDeleteOperation, CMDJsonInstanceCreateAction, CMDJsonInstanceDeleteAction -from utils.case import to_camel_case, to_snake_case + CMDJsonInstanceUpdateAction, CMDJsonSubresourceSelector, CMDInstanceCreateOperation, \ + CMDInstanceDeleteOperation, CMDJsonInstanceCreateAction, CMDJsonInstanceDeleteAction, CMDClientEndpointsByHttpOperation +from utils.case import to_camel_case +from .az_arg_group_generator import AzArgGroupGenerator +from .az_command_ctx import AzCommandCtx from .az_operation_generator import AzHttpOperationGenerator, AzJsonUpdateOperationGenerator, \ - AzGenericUpdateOperationGenerator, AzRequestClsGenerator, AzResponseClsGenerator, \ - AzInstanceUpdateOperationGenerator, AzLifeCycleInstanceUpdateCallbackGenerator, AzJsonCreateOperationGenerator, \ - AzJsonDeleteOperationGenerator, AzLifeCycleCallbackGenerator -from .az_arg_group_generator import AzArgGroupGenerator, AzArgClsGenerator + AzGenericUpdateOperationGenerator, AzInstanceUpdateOperationGenerator, AzLifeCycleInstanceUpdateCallbackGenerator, \ + AzJsonCreateOperationGenerator, AzJsonDeleteOperationGenerator, AzLifeCycleCallbackGenerator from .az_output_generator import AzOutputGenerator from .az_selector_generator import AzJsonSelectorGenerator -from utils import exceptions -import logging - logger = logging.getLogger('backend') -class AzCommandCtx: - - def __init__(self): - self._cls_arg_maps = {} - self._ctx_arg_map = {} - self._selectors = {} - self.rg_arg_var = None - - self.arg_clses = {} - self.update_clses = {} - self.response_clses = {} - self.support_id_part = True - - def set_argument_cls(self, arg): - cls_name = arg.cls - self._cls_arg_maps[f"@{cls_name}"] = {} - assert cls_name not in self.arg_clses, f"Argument class {cls_name} is defined more than one place" - self.arg_clses[cls_name] = AzArgClsGenerator(cls_name, self, arg) - - def set_argument(self, keys, arg, ctx_namespace='self.ctx.args'): - var_name = arg.var - hide = arg.hide - if var_name.startswith('@'): - map_name = var_name.replace('[', '.[').replace('{', '.{').split('.', maxsplit=1)[0] - if map_name != keys[0]: - raise exceptions.VerificationError( - "Invalid argument var", - details=f"argument var '{var_name}' does not start with '{keys[0]}'" - ) - if map_name not in self._cls_arg_maps: - self._cls_arg_maps[map_name] = {} - self._cls_arg_maps[map_name][var_name] = ( - '.'.join(keys).replace('.[', '[').replace('.{', '{'), - hide - ) - else: - self._ctx_arg_map[var_name] = ( - '.'.join([ctx_namespace, *keys]).replace('.[', '[').replace('.{', '{'), - hide - ) - - if isinstance(arg, CMDResourceGroupNameArg): - assert self.rg_arg_var is None, "Resource Group Argument defined twice" - self.rg_arg_var = arg.var - - def get_argument(self, var_name): - if var_name.startswith('@'): - map_name = var_name.replace('[', '.[').replace('{', '.{').split('.', maxsplit=1)[0] - if map_name not in self._cls_arg_maps: - raise exceptions.VerificationError( - "Invalid argument var", - details=f"argument var '{var_name}' has unregistered class '{map_name}'." - ) - if var_name not in self._cls_arg_maps[map_name]: - raise exceptions.VerificationError( - "Invalid argument var", - details=f"argument var '{var_name}' does not find." - ) - return self._cls_arg_maps[map_name][var_name] - else: - if var_name not in self._ctx_arg_map: - raise exceptions.VerificationError( - "Invalid argument var", - details=f"argument var '{var_name}' does not find." - ) - return self._ctx_arg_map[var_name] - - def get_variant(self, variant, name_only=False): - if variant.startswith('$'): - variant = variant[1:] - variant = to_snake_case(variant) - if name_only: - return variant - - is_selector = variant in self._selectors - - if is_selector: - return f'self.ctx.selectors.{variant}', is_selector - else: - return f'self.ctx.vars.{variant}', is_selector - - def set_update_cls(self, schema): - cls_name = schema.cls - assert cls_name not in self.update_clses, f"Schema cls '{cls_name}', is defined more than once" - self.update_clses[cls_name] = AzRequestClsGenerator(self, cls_name, schema) - - def set_response_cls(self, schema): - cls_name = schema.cls - assert cls_name not in self.response_clses, f"Schema cls '{cls_name}', is defined more than once" - self.response_clses[cls_name] = AzResponseClsGenerator(self, cls_name, schema) - - def set_selector(self, selector): - self._selectors[self.get_variant(selector.var, name_only=True)] = selector - - def render_arg_resource_id_template(self, template): - # TODO: fill blank placeholders as much as possible - - return template - - class AzCommandGenerator: - ARGS_SCHEMA_NAME = "_args_schema" def __init__(self, cmd: CLIAtomicCommand, client: CLIAtomicClient, is_wait=False): @@ -143,6 +41,9 @@ def __init__(self, cmd: CLIAtomicCommand, client: CLIAtomicClient, is_wait=False if self.client.cfg and self.client.cfg.arg_group and self.client.cfg.arg_group.args: # add client args self.arg_groups.append(AzArgGroupGenerator(self.ARGS_SCHEMA_NAME, self.cmd_ctx, self.client.cfg.arg_group)) + if isinstance(self.client.cfg.endpoints, CMDClientEndpointsByHttpOperation): + # disable id part if client using http operation to fetch dynamic endpoints + self.cmd_ctx.support_id_part = False if self.cmd.cfg.arg_groups: for arg_group in self.cmd.cfg.arg_groups: @@ -238,7 +139,7 @@ def __init__(self, cmd: CLIAtomicCommand, client: CLIAtomicClient, is_wait=False else: op = AzGenericUpdateOperationGenerator(self.cmd_ctx, variant_key, is_selector_variant) self.json_instance_operations.append(op) - self.operations = [*self.operations[:max_idx+1], op, *self.operations[max_idx+1:]] + self.operations = [*self.operations[:max_idx + 1], op, *self.operations[max_idx + 1:]] # Add instance update callbacks first_instance_op_idx = None @@ -249,11 +150,15 @@ def __init__(self, cmd: CLIAtomicCommand, client: CLIAtomicClient, is_wait=False first_instance_op_idx = idx last_instance_op_idx = idx if last_instance_op_idx is not None and len(self.operations) > last_instance_op_idx + 1: - post_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('post_instance_update', self.operations[last_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) - self.operations = [*self.operations[:last_instance_op_idx+1], post_op_generator, *self.operations[last_instance_op_idx+1:]] + post_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('post_instance_update', self.operations[ + last_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) + self.operations = [*self.operations[:last_instance_op_idx + 1], post_op_generator, + *self.operations[last_instance_op_idx + 1:]] if first_instance_op_idx is not None and first_instance_op_idx > 0: - pre_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('pre_instance_update', self.operations[first_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) - self.operations = [*self.operations[:first_instance_op_idx], pre_op_generator, *self.operations[first_instance_op_idx:]] + pre_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('pre_instance_update', self.operations[ + first_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) + self.operations = [*self.operations[:first_instance_op_idx], pre_op_generator, + *self.operations[first_instance_op_idx:]] # Add instance create callbacks first_instance_op_idx = None @@ -264,11 +169,14 @@ def __init__(self, cmd: CLIAtomicCommand, client: CLIAtomicClient, is_wait=False first_instance_op_idx = idx last_instance_op_idx = idx if last_instance_op_idx is not None and len(self.operations) > last_instance_op_idx + 1: - post_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('post_instance_create', self.operations[last_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) - self.operations = [*self.operations[:last_instance_op_idx+1], post_op_generator, *self.operations[last_instance_op_idx+1:]] + post_op_generator = AzLifeCycleInstanceUpdateCallbackGenerator('post_instance_create', self.operations[ + last_instance_op_idx].variant_key, self.operations[last_instance_op_idx].is_selector_variant) + self.operations = [*self.operations[:last_instance_op_idx + 1], post_op_generator, + *self.operations[last_instance_op_idx + 1:]] if first_instance_op_idx is not None and first_instance_op_idx > 0: pre_op_generator = AzLifeCycleCallbackGenerator('pre_instance_create') - self.operations = [*self.operations[:first_instance_op_idx], pre_op_generator, *self.operations[first_instance_op_idx:]] + self.operations = [*self.operations[:first_instance_op_idx], pre_op_generator, + *self.operations[first_instance_op_idx:]] # Add instance delete callbacks first_instance_op_idx = None diff --git a/src/aaz_dev/cli/controller/az_selector_generator.py b/src/aaz_dev/cli/controller/az_selector_generator.py index 0c37a0b8..94111e78 100644 --- a/src/aaz_dev/cli/controller/az_selector_generator.py +++ b/src/aaz_dev/cli/controller/az_selector_generator.py @@ -1,6 +1,6 @@ from command.model.configuration import CMDJsonSubresourceSelector, CMDArrayIndexBase, CMDObjectIndexBase, \ CMDObjectIndexDiscriminator, CMDObjectIndexAdditionalProperties, CMDObjectIndex, CMDArrayIndex, CMDSchema, \ - CMDObjectIndexAdditionalProperties, CMDSelectorIndex + CMDObjectIndexAdditionalProperties, CMDSelectorIndex, CMDSimpleIndexBase, CMDSimpleIndex from utils.case import to_camel_case @@ -37,7 +37,7 @@ def _iter_scopes(self, is_set=False): def _iter_selector_scopes_by_index(index, scope_name, scope_define, idx_lines, cmd_ctx, is_set): - assert isinstance(index, (CMDObjectIndex, CMDArrayIndex)) + assert isinstance(index, (CMDObjectIndex, CMDArrayIndex, CMDSimpleIndex)) if scope_define is not None: yield scope_name, scope_define, idx_lines, None, None, False scope_define = f"{scope_name}.{index.name}" @@ -69,7 +69,11 @@ def _handle_idx_lines_for_end(): idx_lines[0] = f"idx = next(filters, [{arg_keys}.to_serialized_data()])[0]" return - if isinstance(index, CMDObjectIndexBase): + if isinstance(index, CMDSimpleIndexBase): + is_end = True + _handle_idx_lines_for_end() + yield scope_name, scope_define, idx_lines, None, None, is_end + elif isinstance(index, CMDObjectIndexBase): is_end = False if index.prop: assert isinstance(index.prop, CMDSelectorIndex) diff --git a/src/aaz_dev/cli/model/atomic/_client.py b/src/aaz_dev/cli/model/atomic/_client.py index abb98558..ab931918 100644 --- a/src/aaz_dev/cli/model/atomic/_client.py +++ b/src/aaz_dev/cli/model/atomic/_client.py @@ -13,6 +13,11 @@ class CLIAtomicClient(Model): serialized_name="registeredName", deserialize_from="registeredName" ) + cls_name = StringType( + required=True, + serialized_name="clsName", + deserialize_from="clsName", + ) cfg = CLIClientConfigField() class Options: diff --git a/src/aaz_dev/cli/templates/aaz/profile/_clients.py.j2 b/src/aaz_dev/cli/templates/aaz/profile/_clients.py.j2 index e9b88bdf..8be2be95 100644 --- a/src/aaz_dev/cli/templates/aaz/profile/_clients.py.j2 +++ b/src/aaz_dev/cli/templates/aaz/profile/_clients.py.j2 @@ -1,4 +1,5 @@ {% extends "python.j2" %} +{% import "macros.j2" as fn %} {% block pylint %} # pylint: skip-file # flake8: noqa @@ -12,13 +13,17 @@ from azure.cli.core.aaz import * {%- for client in leaf.iter_clients() %} @register_client({{ client.registered_name|constant_convert }}) -class {{ client.registered_name }}(AAZBaseClient): +class {{ client.cls_name }}(AAZBaseClient): + {%- if client.endpoints.type == "template" %} _CLOUD_HOST_TEMPLATES = { - {%- for cloud, template in client.iter_hosts() %} + {%- for cloud, template in client.endpoints.iter_hosts() %} CloudNameEnum.{{cloud}}: {{ template|constant_convert }}, {%- endfor %} } + {%- elif client.endpoints.type == "http-operation" %} + + {%- endif %} _AAD_CREDENTIAL_SCOPES = [ {%- for scope in client.aad_scopes %} @@ -28,7 +33,15 @@ class {{ client.registered_name }}(AAZBaseClient): @classmethod def _build_base_url(cls, ctx, **kwargs): + {%- if client.endpoints.type == "template" %} return cls._CLOUD_HOST_TEMPLATES.get(ctx.cli_ctx.cloud.name, None) + {%- elif client.endpoints.type == "http-operation" %} + cls._fetch_endpoint(ctx, **kwargs) + host = ctx.selectors.{{client.endpoints.selector.name}}.required().to_serialized_data() + if not isinstance(host, str): + raise ValueError(f"Invalid host value: '{host}'") + return host + {%- endif %} @classmethod def _build_configuration(cls, ctx, credential, **kwargs): @@ -37,12 +50,24 @@ class {{ client.registered_name }}(AAZBaseClient): credential_scopes=cls._AAD_CREDENTIAL_SCOPES, **kwargs ) + + {%- if client.endpoints.type == "http-operation" %} + + @classmethod + def _fetch_endpoint(cls, ctx, **kwargs): + cls.{{ client.endpoints.selector.cls_name }}(ctx=ctx, name={{client.endpoints.selector.name|constant_convert}}) + cls.{{ client.endpoints.operation.name }}(ctx=ctx)() + {{ fn.generate_json_selector(client.endpoints.selector) }} + {{ fn.generate_http_operation(client.endpoints.operation, client.endpoints.client_name, client.helper_cls_name, false) }} + {%- endif %} + +{{ fn.generate_helper_cls(client) }} {{ "" }} {%- endfor %} __all__ = [ {%- for client in leaf.iter_clients() %} - {{ client.registered_name|constant_convert }}, + {{ client.cls_name|constant_convert }}, {%- endfor %} ] {% endblock %} \ No newline at end of file diff --git a/src/aaz_dev/cli/templates/macros.j2 b/src/aaz_dev/cli/templates/macros.j2 new file mode 100644 index 00000000..f41867c6 --- /dev/null +++ b/src/aaz_dev/cli/templates/macros.j2 @@ -0,0 +1,466 @@ +{%- macro generate_json_selector(selector) %} + class {{selector.cls_name}}(AAZJsonSelector): + + def _get(self): + {%- for scope, scope_define, idx_lines, filter_builder, filters, is_end in selector.iter_scopes_for_get() %} + + {%- if is_end %} + {%- for line in idx_lines %} + {{line}} + {%- endfor %} + return {{ scope_define }} + + {%- else %} + {%- for line in idx_lines %} + {{line}} + {%- endfor %} + {{ scope }} = {{ scope_define }} + + {%- if filter_builder is not none %} + filters = {{ filter_builder }} + {%- for key, value, is_constant in filters %} + filters = filter( + {%- if is_constant %} + lambda e: e{{key}} == {{ value|constant_convert }}, + {%- else %} + lambda e: e{{key}} == {{ value }}, + {%- endif %} + filters + ) + {%- endfor %} + {%- endif %} + + {%- endif %} + + {%- endfor %} + + def _set(self, value): + {%- for scope, scope_define, idx_lines, filter_builder, filters, is_end in selector.iter_scopes_for_set() %} + + {%- if is_end %} + {%- for line in idx_lines %} + {{line}} + {%- endfor %} + {{ scope_define }} = value + return + + {%- else %} + {%- for line in idx_lines %} + {{line}} + {%- endfor %} + {{ scope }} = {{ scope_define }} + + {%- if filter_builder is not none %} + filters = {{ filter_builder }} + {%- for key, value, is_constant in filters %} + filters = filter( + {%- if is_constant %} + lambda e: e{{key}} == {{ value|constant_convert }}, + {%- else %} + lambda e: e{{key}} == {{ value }}, + {%- endif %} + filters + ) + {%- endfor %} + {%- endif %} + + {%- endif %} + + {%- endfor %} +{%- endmacro %} + +{%- macro generate_http_operation(op, client_registered_name, helper_cls_name, support_no_wait) %} + class {{ op.name }}(AAZHttpOperation): + CLIENT_TYPE = "{{ client_registered_name }}" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + + {%- if op.success_202_response %} + if session.http_response.status_code in {{ op.success_202_response.status_codes|constant_convert }}: + return self.client.build_lro_polling( + {% if support_no_wait %}self.ctx.args.no_wait{% else %}False{% endif %}, + session, + {%- if op.success_202_response.callback_name is not none %} + self.{{ op.success_202_response.callback_name }}, + {%- else %} + None, + {%- endif %} + self.on_error, + lro_options={{ op.lro_options|constant_convert }}, + path_format_arguments=self.url_parameters, + ) + {%- endif %} + + {%- for response in op.success_responses %} + if session.http_response.status_code in {{ response.status_codes|constant_convert }}: + {%- if op.is_long_running %} + return self.client.build_lro_polling( + {% if support_no_wait %}self.ctx.args.no_wait{% else %}False{% endif %}, + session, + self.{{ response.callback_name }}, + self.on_error, + lro_options={{ op.lro_options|constant_convert }}, + path_format_arguments=self.url_parameters, + ) + {%- else %} + return self.{{ response.callback_name }}(session) + {%- endif %} + {%- endfor %} + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "{{ op.url }}", + **self.url_parameters + ) + + @property + def method(self): + return "{{ op.method }}" + + @property + def error_format(self): + return "{{ op.error_format }}" + + {%- if op.url_parameters is not none and op.url_parameters|length %} + + @property + def url_parameters(self): + parameters = { + {%- for name, data, is_constant, kwargs in op.url_parameters %} + {%- if is_constant %} + **self.serialize_url_param( + "{{ name }}", {{ data|constant_convert }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- else %} + **self.serialize_url_param( + "{{ name }}", {{ data }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- endif %} + {%- endfor %} + } + return parameters + {%- endif %} + + {%- if op.query_parameters is not none and op.query_parameters | length %} + + @property + def query_parameters(self): + parameters = { + {%- for name, data, is_constant, kwargs in op.query_parameters %} + {%- if is_constant %} + **self.serialize_query_param( + "{{ name }}", {{ data|constant_convert }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- else %} + **self.serialize_query_param( + "{{ name }}", {{ data }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- endif %} + {%- endfor %} + } + return parameters + {%- endif %} + + {%- if op.header_parameters is not none and op.header_parameters | length %} + + @property + def header_parameters(self): + parameters = { + {%- for name, data, is_constant, kwargs in op.header_parameters %} + {%- if is_constant %} + **self.serialize_header_param( + "{{ name }}", {{ data|constant_convert }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- else %} + **self.serialize_header_param( + "{{ name }}", {{ data }}, + {%- for key, value in kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ), + {%- endif %} + {%- endfor %} + } + return parameters + {%- endif %} + + {%- if op.content is not none %} + + @property + def content(self): + {{ op.content.VALUE_NAME}}, {{ op.content.BUILDER_NAME }} = self.new_content_builder( + {{ op.content.arg_key }}, + {%- if op.content.ref is not none %} + value={{ op.content.ref }}, + {%- else %} + typ={{ op.content.typ }}, + {%- if op.content.typ_kwargs is not none and op.content.typ_kwargs|length %} + typ_kwargs={{ op.content.typ_kwargs|constant_convert }} + {%- endif %} + {%- endif %} + ) + + {%- if op.content.ref is none %} + {%- if op.content.cls_builder_name is not none %} + {{ helper_cls_name }}.{{ op.content.cls_builder_name }}({{ op.content.BUILDER_NAME }}) + {%- endif %} + + {%- for scope, scope_define, props, discriminators in op.content.iter_scopes() %} + + {%- if scope_define is not none %} + + {{ scope }} = {{ op.content.BUILDER_NAME }}.get({{ scope_define|constant_convert }}) + if {{ scope }} is not None: + {%- endif %} + + {%- for prop_name, prop_type, is_const, const_value, arg_key, prop_type_kwargs, cls_builder_name in props %} + {% if scope_define is not none %}{{ " " }}{% endif %} + {%- if cls_builder_name is not none %}{{ helper_cls_name }}.{{ cls_builder_name }}({% endif %} + {%- if prop_name != '[]' and prop_name != '{}' -%} + {%- if is_const -%} + {{ scope }}.set_const({{ prop_name|constant_convert }}, {{ const_value|constant_convert }}, {{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- else -%} + {{ scope }}.set_prop({{ prop_name|constant_convert }}, {{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- endif %} + {%- elif prop_name == '{}' and prop_type is none -%} + {{ scope }}.set_anytype_elements({%- if arg_key is not none %}{{ arg_key|constant_convert }}{% endif %}) + {%- else -%} + {{ scope }}.set_elements({{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- endif %} + {%- if cls_builder_name is not none %}){% endif %} + {%- endfor %} + + {%- for disc_name, disc_value in discriminators %} + {% if scope_define is not none %}{{ " " }}{% endif -%} + {{ scope }}.discriminate_by({{ disc_name|constant_convert }}, {{ disc_value|constant_convert }}) + {%- endfor %} + + {%- endfor %} + {%- endif %} + + return self.serialize_content({{ op.content.VALUE_NAME }}) + {%- endif %} + + {%- if op.form_content is not none %} + + @property + def form_content(self): + # TODO: + return None + {%- endif %} + + {%- if op.stream_content is not none %} + + @property + def stream_content(self): + # TODO: + return None + {%- endif %} + + {%- for response in op.success_responses %} + + def {{ response.callback_name }}(self, session): + {%- if response.variant_name is not none %} + data = self.deserialize_http_content(session) + self.ctx.set_var( + "{{ response.variant_name }}", + data, + schema_builder=self.{{ response.schema.builder_name }} + ) + {%- else %} + pass + {%- endif %} + + {%- if response.variant_name is not none %} + + {{ response.schema.name }} = None + + @classmethod + def {{ response.schema.builder_name }}(cls): + if cls.{{ response.schema.name }} is not None: + return cls.{{ response.schema.name }} + + cls.{{ response.schema.name }} = {{ response.schema.typ }}( + {%- if not response.schema.typ_kwargs|length %}) + {%- else %} + {%- for key, value in response.schema.typ_kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ) + {%- endif %} + {%- if response.schema.cls_builder_name is not none %} + {{ helper_cls_name }}.{{ response.schema.cls_builder_name }}(cls.{{ response.schema.name }}) + {%- endif %} + + {%- for scope, scope_define, props in response.schema.iter_scopes() %} + + {{ scope }} = {{ scope_define }} + + {%- for prop_name, prop_type, prop_kwargs, cls_builder_name in props %} + {{ scope }}{{ prop_name|get_prop }} = {{ prop_type }}( + {%- if not prop_kwargs|length %}) + {%- else %} + {%- for key, value in prop_kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ) + {%- endif %} + {%- if cls_builder_name is not none %} + {{ helper_cls_name }}.{{ cls_builder_name }}({{ scope }}{{ prop_name|get_prop }}) + {%- endif %} + {%- endfor %} + + {%- endfor %} + + return cls.{{ response.schema.name }} + {%- endif %} + + {%- endfor %} +{%- endmacro %} + +{%- macro generate_helper_cls(leaf_cls) %} +class {{ leaf_cls.helper_cls_name }}: + """Helper class for {{ leaf_cls.cls_name }}""" + + {%- for update_cls in leaf_cls.get_update_clses() %} + + @classmethod + def {{ update_cls.builder_name }}(cls, {{ update_cls.BUILDER_NAME }}): + if {{ update_cls.BUILDER_NAME }} is None: + return + + {%- for scope, scope_define, props, discriminators in update_cls.iter_scopes() %} + + {%- if scope_define is not none %} + + {{ scope }} = {{ update_cls.BUILDER_NAME }}.get({{ scope_define|constant_convert }}) + if {{ scope }} is not None: + {%- endif %} + + {%- for prop_name, prop_type, is_const, const_value, arg_key, prop_type_kwargs, cls_builder_name in props %} + {% if scope_define is not none %}{{ " " }}{% endif %} + {%- if cls_builder_name is not none %}cls.{{ cls_builder_name }}({% endif %} + {%- if prop_name != '[]' and prop_name != '{}' -%} + {%- if is_const -%} + {{ scope }}.set_const({{ prop_name|constant_convert }}, {{ const_value|constant_convert }}, {{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- else -%} + {{ scope }}.set_prop({{ prop_name|constant_convert }}, {{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- endif %} + {%- elif prop_name == '{}' and prop_type is none -%} + {{ scope }}.set_anytype_elements({%- if arg_key is not none %}{{ arg_key|constant_convert }}{% endif %}) + {%- else -%} + {{ scope }}.set_elements({{ prop_type }} + {%- if arg_key is not none %}, {{ arg_key|constant_convert }}{% endif %} + {%- if prop_type_kwargs is not none and prop_type_kwargs|length %}, typ_kwargs={{ prop_type_kwargs|constant_convert }}{% endif %}) + {%- endif %} + {%- if cls_builder_name is not none %}){% endif %} + {%- endfor %} + + {%- for disc_name, disc_value in discriminators %} + {% if scope_define is not none %}{{ " " }}{% endif -%} + {{ scope }}.discriminate_by({{ disc_name|constant_convert }}, {{ disc_value|constant_convert }}) + {%- endfor %} + + {%- endfor %} + + + {%- endfor %} + + {%- for resp_cls in leaf_cls.get_response_clses() %} + + {{ resp_cls.schema_name }} = None + + @classmethod + def {{ resp_cls.builder_name }}(cls, _schema): + if cls.{{ resp_cls.schema_name }} is not None: + {%- for prop_name in resp_cls.props %} + _schema{{ prop_name|get_prop }} = cls.{{ resp_cls.schema_name }}{{ prop_name|get_prop }} + {%- endfor %} + {%- for disc_key, disc_value in resp_cls.discriminators %} + _schema.discriminate_by( + {{ disc_key|constant_convert }}, + {{ disc_value|constant_convert }}, + cls.{{ resp_cls.schema_name }}.discriminate_by( + {{ disc_key|constant_convert }}, + {{ disc_value|constant_convert }}, + ) + ) + {%- endfor %} + return + + cls.{{ resp_cls.schema_name }} = {{ resp_cls.schema_name }} = {{ resp_cls.typ }}( + {%- if not resp_cls.typ_kwargs|length %}) + {%- else %} + {%- for key, value in resp_cls.typ_kwargs.items() %} + {{ key }}={{ value|constant_convert }} + {%- endfor %} + ) + {%- endif %} + + {%- for scope, scope_define, props in resp_cls.iter_scopes() %} + + {{ scope }} = {{ scope_define }} + + {%- for prop_name, prop_type, prop_kwargs, cls_builder_name in props %} + {{ scope }}{{ prop_name|get_prop }} = {{ prop_type }}( + {%- if not prop_kwargs|length %}) + {%- else %} + {%- for key, value in prop_kwargs.items() %} + {{ key }}={{ value|constant_convert }}, + {%- endfor %} + ) + {%- endif %} + {%- if cls_builder_name is not none %} + cls.{{ cls_builder_name }}({{ scope }}{{ prop_name|get_prop }}) + {%- endif %} + {%- endfor %} + + {%- endfor %} + {{- "\n" }} + {%- for prop_name in resp_cls.props %} + _schema{{ prop_name|get_prop }} = cls.{{ resp_cls.schema_name }}{{ prop_name|get_prop }} + {%- endfor %} + {%- for disc_key, disc_value in resp_cls.discriminators %} + _schema.discriminate_by( + {{ disc_key|constant_convert }}, + {{ disc_value|constant_convert }}, + cls.{{ resp_cls.schema_name }}.discriminate_by( + {{ disc_key|constant_convert }}, + {{ disc_value|constant_convert }}, + ) + ) + {%- endfor %} + + + {%- endfor %} +{%- endmacro %} diff --git a/src/aaz_dev/cli/tests/api_tests/test_az.py b/src/aaz_dev/cli/tests/api_tests/test_az.py index fc88640c..5004e6ad 100644 --- a/src/aaz_dev/cli/tests/api_tests/test_az.py +++ b/src/aaz_dev/cli/tests/api_tests/test_az.py @@ -537,3 +537,114 @@ def test_generate_data_plane_in_main_repo(self): self.assertTrue(rv.status_code == 200) data = rv.get_json() self.assertEqual(data['profiles'], profiles) + + def test_generate_data_plane_with_dynamic_endpoints_in_main_repo(self): + self.prepare_aaz() + mod_name = "aaz-data-plane-dynamic" + manager = AzMainManager() + path = manager.get_mod_path(mod_name) + if os.path.exists(path): + shutil.rmtree(path, ignore_errors=True) + + with self.app.test_client() as c: + rv = c.post(f"/CLI/Az/Main/Modules", json={ + "name": mod_name + }) + self.assertTrue(rv.status_code == 200) + profiles = rv.get_json()['profiles'] + + # latest profile + version = '2021-06-01-preview' + profile = profiles[Config.CLI_DEFAULT_PROFILE] + profile['commandGroups'] = { + 'attestation': { + 'names': ['attestation'], + 'commandGroups': { + 'provider': { + 'names': ['attestation', 'provider'], + 'commands': { + 'create': { + 'names': ['attestation', 'provider', 'create'], + 'registered': True, + 'version': version, + }, + 'delete': { + 'names': ['attestation', 'provider', 'delete'], + 'registered': True, + 'version': version, + }, + 'list': { + 'names': ['attestation', 'provider', 'list'], + 'registered': True, + 'version': version, + }, + 'show': { + 'names': ['attestation', 'provider', 'show'], + 'registered': True, + 'version': version, + }, + 'update': { + 'names': ['attestation', 'provider', 'update'], + 'registered': True, + 'version': version, + } + } + }, + } + } + } + + version = '2022-09-01-preview' + profile['commandGroups']['attestation']['commandGroups']['policy'] = { + 'names': ['attestation', 'policy'], + 'commands': { + 'set': { + 'names': ['attestation', 'policy', 'set'], + 'registered': True, + 'version': version, + }, + 'reset': { + 'names': ['attestation', 'policy', 'reset'], + 'registered': True, + 'version': version, + }, + 'show': { + 'names': ['attestation', 'policy', 'show'], + 'registered': True, + 'version': version, + }, + + } + } + + profile['commandGroups']['attestation']['commandGroups']['signer'] = { + 'names': ['attestation', 'signer'], + 'commands': { + 'add': { + 'names': ['attestation', 'signer', 'add'], + 'registered': True, + 'version': version, + }, + 'list': { + 'names': ['attestation', 'signer', 'list'], + 'registered': True, + 'version': version, + }, + 'remove': { + 'names': ['attestation', 'signer', 'remove'], + 'registered': True, + 'version': version, + }, + + } + } + + rv = c.put(f"/CLI/Az/Main/Modules/{mod_name}", json={ + "profiles": profiles + }) + self.assertTrue(rv.status_code == 200) + + rv = c.get(f"/CLI/Az/Main/Modules/{mod_name}") + self.assertTrue(rv.status_code == 200) + data = rv.get_json() + self.assertEqual(data['profiles'], profiles) diff --git a/src/aaz_dev/cli/tests/common.py b/src/aaz_dev/cli/tests/common.py index cb43ec89..eb92574e 100644 --- a/src/aaz_dev/cli/tests/common.py +++ b/src/aaz_dev/cli/tests/common.py @@ -25,10 +25,12 @@ def prepare_aaz(self): # self.prepare_elastic_aaz_2020_07_01() # self.prepare_elastic_aaz_2021_09_01_preview() # self.prepare_elastic_aaz_2021_10_01_preview() + self.prepare_attestation_aaz_2021_06_01_preview() # data plane self.prepare_codesigning_aaz_2023_06_15_preview() self.prepare_monitor_metrics_aaz_2023_05_01_preview() + self.prepare_attestation_aaz_2022_09_01_preview() @workspace_name("prepare_edge_order_aaz_2020_12_01_preview") def prepare_edge_order_aaz_2020_12_01_preview(self, ws_name): @@ -2667,14 +2669,14 @@ def prepare_codesigning_aaz_2023_06_15_preview(self, ws_name): {'arg': '$Client.Endpoint.region', 'name': 'region', 'required': True, 'skipUrlEncoding': True, 'type': 'string'} ]) self.assertEqual(client_config['argGroup']['args'], [ - {'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'} + {'group': 'Client', 'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'} ]) # update client arguments rv = c.get(f"{ws_url}/ClientConfig/Arguments/$Client.Endpoint.region") self.assertTrue(rv.status_code == 200) client_arg = rv.get_json() - self.assertEqual(client_arg, {'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'}) + self.assertEqual(client_arg, {'group': 'Client', 'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'}) rv = c.patch(f"{ws_url}/ClientConfig/Arguments/$Client.Endpoint.region", json={ "options": ["region", "r"], "default": { @@ -2687,6 +2689,7 @@ def prepare_codesigning_aaz_2023_06_15_preview(self, ws_name): self.assertTrue(rv.status_code == 200) client_arg = rv.get_json() self.assertEqual(client_arg, { + 'group': 'Client', 'default': {'value': 'global'}, 'help': {'short': 'The Azure region wherein requests for signing will be sent.'}, 'options': ['r', 'region'], @@ -2814,7 +2817,7 @@ def prepare_monitor_metrics_aaz_2023_05_01_preview(self, ws_name): rv = c.get(f"{ws_url}/ClientConfig/Arguments/$Client.Endpoint.region") self.assertTrue(rv.status_code == 200) client_arg = rv.get_json() - self.assertEqual(client_arg, {'options': ['region'], 'required': True, 'type': 'string', + self.assertEqual(client_arg, {'group': 'Client', 'options': ['region'], 'required': True, 'type': 'string', 'var': '$Client.Endpoint.region'}) # add resources @@ -2861,3 +2864,172 @@ def prepare_monitor_metrics_aaz_2023_05_01_preview(self, ws_name): rv = c.post(f"{ws_url}/Generate") self.assertTrue(rv.status_code == 200) + + @workspace_name("prepare_attestation_aaz_2021_06_01_preview") + def prepare_attestation_aaz_2021_06_01_preview(self, ws_name): + module = "attestation" + resource_provider = "Microsoft.Attestation" + api_version = '2021-06-01-preview' + + with self.app.test_client() as c: + rv = c.post(f"/AAZ/Editor/Workspaces", json={ + "name": ws_name, + "plane": PlaneEnum.Mgmt, + "modNames": module, + "resourceProvider": resource_provider + }) + self.assertTrue(rv.status_code == 200) + ws = rv.get_json() + self.assertEqual(ws['plane'], PlaneEnum.Mgmt) + self.assertEqual(ws['resourceProvider'], resource_provider) + self.assertEqual(ws['modNames'], module) + ws_url = ws['url'] + + # add resources + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger", json={ + 'module': module, + 'version': api_version, + 'resources': [ + {'id': swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}')}, + {'id': swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/providers/Microsoft.Attestation/attestationProviders')}, + {'id': swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders')}, + ] + }) + self.assertTrue(rv.status_code == 200) + + command_tree = rv.get_json() + + # modify command tree + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/attestation-provider/Rename", json={ + "name": "attestation provider" + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/provider", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.get(f"{ws_url}/CommandTree/Nodes/aaz") + self.assertTrue(rv.status_code == 200) + command_tree = rv.get_json() + + rv = c.post(f"{ws_url}/Generate") + self.assertTrue(rv.status_code == 200) + + @workspace_name("prepare_attestation_aaz_2022_09_01_preview") + def prepare_attestation_aaz_2022_09_01_preview(self, ws_name): + module = "attestation" + resource_provider = "Microsoft.Attestation" + api_version = '2022-09-01-preview' + + with self.app.test_client() as c: + rv = c.post(f"/AAZ/Editor/Workspaces", json={ + "name": ws_name, + "plane": PlaneEnum._Data, + "modNames": module, + "resourceProvider": resource_provider + }) + self.assertTrue(rv.status_code == 200) + ws = rv.get_json() + self.assertEqual(ws['plane'], PlaneEnum.Data(resource_provider)) + self.assertEqual(ws['resourceProvider'], resource_provider) + self.assertEqual(ws['modNames'], module) + ws_url = ws['url'] + + # add client configuration + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01", + "subresource": "properties.attestUri", + }, + }) + self.assertTrue(rv.status_code == 200) + rv = c.get(f"{ws_url}/ClientConfig") + + # add resources + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/AddSwagger", json={ + 'module': module, + 'version': api_version, + 'resources': [ + {'id': swagger_resource_path_to_resource_id('/policies/{attestationType}'), + 'options': {'update_by': 'None'}}, + {'id': swagger_resource_path_to_resource_id('/policies/{attestationType}:reset')}, + {'id': swagger_resource_path_to_resource_id('/certificates')}, + {'id': swagger_resource_path_to_resource_id('/certificates:add')}, + {'id': swagger_resource_path_to_resource_id('/certificates:remove')}, + ] + }) + self.assertTrue(rv.status_code == 200) + + command_tree = rv.get_json() + + # modify command tree + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/policy/Leaves/create/Rename", json={ + "name": "attestation policy set" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/certificate/Leaves/show/Rename", json={ + "name": "attestation signer list" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/Leaves/certificatesadd/Rename", json={ + "name": "attestation signer add" + }) + self.assertTrue(rv.status_code == 200) + rv = c.post(f"{ws_url}/CommandTree/Nodes/aaz/attestation/Leaves/certificatesremove/Rename", json={ + "name": "attestation signer remove" + }) + self.assertTrue(rv.status_code == 200) + + rv = c.delete(f"{ws_url}/CommandTree/Nodes/aaz/attestation/certificate") + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/signer", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.patch(f"{ws_url}/CommandTree/Nodes/aaz/attestation/policy", json={ + "help": { + "short": "test" + } + }) + self.assertTrue(rv.status_code == 200) + + rv = c.get(f"{ws_url}/CommandTree/Nodes/aaz") + self.assertTrue(rv.status_code == 200) + command_tree = rv.get_json() + + rv = c.post(f"{ws_url}/Generate") + self.assertTrue(rv.status_code == 200) From 4bd42a38e31d9961e09c54a04ae4fa3c20233a18 Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Wed, 22 Nov 2023 14:04:02 +0800 Subject: [PATCH 5/9] UI support to load dynamic endpoints --- src/web/src/views/workspace/WSEditor.tsx | 47 +++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/web/src/views/workspace/WSEditor.tsx b/src/web/src/views/workspace/WSEditor.tsx index c0c30032..9b25e226 100644 --- a/src/web/src/views/workspace/WSEditor.tsx +++ b/src/web/src/views/workspace/WSEditor.tsx @@ -39,7 +39,8 @@ interface ClientAuth { interface ClientConfig { version: string, - templates: ClientTemplateMap, + endpointTemplates?: ClientTemplateMap, + endpointResource?: Resource, auth: ClientAuth, } @@ -294,12 +295,19 @@ class WSEditor extends React.Component { let res = await axios.get(`${workspaceUrl}/ClientConfig`); const clientConfig: ClientConfig = { version: res.data.version, - templates: {}, + endpointTemplates: undefined, + endpointResource: undefined, auth: res.data.auth, } - res.data.endpoints.templates.forEach((value: any) => { - clientConfig.templates[value.cloud] = value.template; - }); + if (res.data.endpoints.type === "template") { + clientConfig.endpointTemplates = {}; + res.data.endpoints.templates.forEach((value: any) => { + clientConfig.endpointTemplates![value.cloud] = value.template; + }); + } else if (res.data.endpoints.type === "http-operation") { + clientConfig.endpointResource = res.data.endpoints.endpointResource; + } + return clientConfig; } catch (err: any) { // catch 404 error @@ -1118,18 +1126,31 @@ class WSEditorClientConfigDialog extends React.Component { - clientConfig.templates[value.cloud] = value.template; - }); + let templateAzureCloud = ""; + let templateAzureChinaCloud = ""; + let templateAzureUSGovernment = ""; + let templateAzureGermanCloud = ""; + if (res.data.endpoints.type === "template") { + clientConfig.endpointTemplates = {}; + res.data.endpoints.templates.forEach((value: any) => { + clientConfig.endpointTemplates![value.cloud] = value.template; + }); + templateAzureCloud = clientConfig.endpointTemplates!['AzureCloud'] ?? ""; + templateAzureChinaCloud = clientConfig.endpointTemplates!['AzureChinaCloud'] ?? ""; + templateAzureUSGovernment = clientConfig.endpointTemplates!['AzureUSGovernment'] ?? ""; + templateAzureGermanCloud = clientConfig.endpointTemplates!['AzureGermanCloud'] ?? ""; + } else if (res.data.endpoints.type === "http-operation") { + clientConfig.endpointResource = res.data.endpoints.resource; + } + this.setState({ aadAuthScopes: clientConfig.auth.aad.scopes ?? ["",], - templateAzureCloud: clientConfig.templates['AzureCloud'] ?? "", - templateAzureChinaCloud: clientConfig.templates['AzureChinaCloud'] ?? "", - templateAzureUSGovernment: clientConfig.templates['AzureUSGovernment'] ?? "", - templateAzureGermanCloud: clientConfig.templates['AzureGermanCloud'] ?? "", + templateAzureCloud: templateAzureCloud, + templateAzureChinaCloud: templateAzureChinaCloud, + templateAzureUSGovernment: templateAzureUSGovernment, + templateAzureGermanCloud: templateAzureGermanCloud, isAdd: false }); } catch (err: any) { From 3db83b291c82bf6fd347aa449294298a226fa767 Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Wed, 22 Nov 2023 15:51:37 +0800 Subject: [PATCH 6/9] switch info and secondary color definition in theme --- src/web/src/theme.tsx | 10 +- .../cli/CLIModGeneratorProfileCommandTree.tsx | 2 +- src/web/src/views/cli/CLIModuleGenerator.tsx | 2 +- src/web/src/views/workspace/WSEditor.tsx | 231 +++++++++++------- .../WSEditorCommandArgumentsContent.tsx | 16 +- .../workspace/WSEditorCommandContent.tsx | 20 +- .../workspace/WSEditorCommandGroupContent.tsx | 8 +- .../views/workspace/WSEditorCommandTree.tsx | 6 +- 8 files changed, 169 insertions(+), 126 deletions(-) diff --git a/src/web/src/theme.tsx b/src/web/src/theme.tsx index f458241a..232adea9 100644 --- a/src/web/src/theme.tsx +++ b/src/web/src/theme.tsx @@ -9,15 +9,15 @@ const rawTheme = createTheme({ dark: '#1e1e1f', }, secondary: { - light: '#fff5f8', - main: '#ff3366', - dark: '#e62958', - }, - info: { light: '#cccfff', main: '#5d64cf', dark: '#464c99', }, + info: { + light: '#fff5f8', + main: '#ff3366', + dark: '#e62958', + }, warning: { main: '#ffc071', dark: '#ffb25e', diff --git a/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx b/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx index ece906a3..54e3545f 100644 --- a/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx +++ b/src/web/src/views/cli/CLIModGeneratorProfileCommandTree.tsx @@ -125,7 +125,7 @@ class CLIModGeneratorProfileCommandTree extends React.Component } - {command.modified && } + {command.modified && } {command.selectedVersion !== undefined && {updating && - + } {!updating && diff --git a/src/web/src/views/workspace/WSEditor.tsx b/src/web/src/views/workspace/WSEditor.tsx index 9b25e226..de2e6df3 100644 --- a/src/web/src/views/workspace/WSEditor.tsx +++ b/src/web/src/views/workspace/WSEditor.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box, Dialog, Slide, Drawer, Toolbar, DialogTitle, DialogContent, DialogActions, LinearProgress, Button, List, ListSubheader, Paper, ListItemButton, ListItemIcon, Checkbox, ListItemText, ListItem, TextField, Alert, InputLabel, IconButton, Input, Typography, TypographyProps } from '@mui/material'; +import { Box, Dialog, Slide, Drawer, Toolbar, DialogTitle, DialogContent, DialogActions, LinearProgress, Button, List, ListSubheader, Paper, ListItemButton, ListItemIcon, Checkbox, ListItemText, ListItem, TextField, Alert, InputLabel, IconButton, Input, Typography, TypographyProps, Tabs, Tab } from '@mui/material'; import { useParams } from 'react-router'; import axios from 'axios'; import { TransitionProps } from '@mui/material/transitions'; @@ -307,7 +307,7 @@ class WSEditor extends React.Component { } else if (res.data.endpoints.type === "http-operation") { clientConfig.endpointResource = res.data.endpoints.endpointResource; } - + return clientConfig; } catch (err: any) { // catch 404 error @@ -561,10 +561,10 @@ class WSEditorExportDialog extends React.Component { const url = `${this.props.workspaceUrl}/ClientConfig/AAZ/Compare`; - this.setState({updating: true}); + this.setState({ updating: true }); try { await axios.post(url); - this.setState({clientConfigOOD: false, updating: false}); + this.setState({ clientConfigOOD: false, updating: false }); } catch (err: any) { // catch 409 error if (err.response?.status === 409) { @@ -582,17 +582,17 @@ class WSEditorExportDialog extends React.Component { const url = `${this.props.workspaceUrl}/ClientConfig/AAZ/Inherit`; - this.setState({updating: true}); + this.setState({ updating: true }); try { await axios.post(url); - this.setState({clientConfigOOD: false, updating: false}); + this.setState({ clientConfigOOD: false, updating: false }); this.props.onClose(false, true); } catch (err: any) { console.error(err.response) @@ -602,17 +602,17 @@ class WSEditorExportDialog extends React.Component { const url = `${this.props.workspaceUrl}/Generate`; - this.setState({updating: true}); + this.setState({ updating: true }); try { await axios.post(url); - this.setState({updating: false}); + this.setState({ updating: false }); this.props.onClose(false, false); } catch (err: any) { console.error(err.response) @@ -622,7 +622,7 @@ class WSEditorExportDialog extends React.Component {updating && - + } {!updating && @@ -713,7 +713,7 @@ function WSEditorDeleteDialog(props: { {updating && - + } {!updating && @@ -940,7 +940,7 @@ class WSEditorSwaggerReloadDialog extends React.Component {updating && - + } {!updating && @@ -1061,7 +1061,7 @@ class WSRenameDialog extends React.Component {updating && - + } {!updating && @@ -1085,10 +1085,13 @@ interface WSEditorClientConfigDialogState { invalidText: string | undefined, isAdd: boolean, + endpointType: "template" | "http-operation", + templateAzureCloud: string, templateAzureChinaCloud: string, templateAzureUSGovernment: string, templateAzureGermanCloud: string, + aadAuthScopes: string[], } @@ -1108,6 +1111,9 @@ class WSEditorClientConfigDialog extends React.Component { clientConfig.endpointTemplates![value.cloud] = value.template; }); + + endpointType = "template"; templateAzureCloud = clientConfig.endpointTemplates!['AzureCloud'] ?? ""; templateAzureChinaCloud = clientConfig.endpointTemplates!['AzureChinaCloud'] ?? ""; templateAzureUSGovernment = clientConfig.endpointTemplates!['AzureUSGovernment'] ?? ""; templateAzureGermanCloud = clientConfig.endpointTemplates!['AzureGermanCloud'] ?? ""; } else if (res.data.endpoints.type === "http-operation") { clientConfig.endpointResource = res.data.endpoints.resource; + + endpointType = "http-operation"; } - + this.setState({ aadAuthScopes: clientConfig.auth.aad.scopes ?? ["",], + endpointType: endpointType, templateAzureCloud: templateAzureCloud, templateAzureChinaCloud: templateAzureChinaCloud, templateAzureUSGovernment: templateAzureUSGovernment, @@ -1175,7 +1187,7 @@ class WSEditorClientConfigDialog extends React.Component { - let { aadAuthScopes, templateAzureCloud, templateAzureChinaCloud, templateAzureGermanCloud, templateAzureUSGovernment } = this.state + let { aadAuthScopes, endpointType, templateAzureCloud, templateAzureChinaCloud, templateAzureGermanCloud, templateAzureUSGovernment } = this.state templateAzureCloud = templateAzureCloud.trim(); if (templateAzureCloud.length < 1) { this.setState({ @@ -1335,7 +1347,7 @@ class WSEditorClientConfigDialog extends React.Component{isAdd ? "Setup Client Config" : "Modify Client Config"} {invalidText && {invalidText} } - Endpoint Templates - - { - this.setState({ - templateAzureCloud: event.target.value, - }) - }} - margin='dense' - required - /> - - { - this.setState({ - templateAzureChinaCloud: event.target.value, - }) - }} - margin='normal' - /> - - { - this.setState({ - templateAzureUSGovernment: event.target.value, - }) - }} - margin='normal' - /> - - { - this.setState({ - templateAzureGermanCloud: event.target.value, - }) - }} - margin='normal' - /> + Endpoint + + + { + this.setState({ + endpointType: newValue, + }) + }}> + + + + + {endpointType === "template" && + { + this.setState({ + templateAzureCloud: event.target.value, + }) + }} + margin='dense' + required + /> - - AAD Auth Scopes + { + this.setState({ + templateAzureChinaCloud: event.target.value, + }) + }} + margin='normal' + /> + + { + this.setState({ + templateAzureUSGovernment: event.target.value, + }) + }} + margin='normal' + /> + + { + this.setState({ + templateAzureGermanCloud: event.target.value, + }) + }} + margin='normal' + /> + } + + + AAD Auth Scopes {aadAuthScopes?.map(this.buildAadScopeInput)} {updating && - + } {!updating && diff --git a/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx b/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx index 3afdddac..80ed0fa1 100644 --- a/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx +++ b/src/web/src/views/workspace/WSEditorCommandArgumentsContent.tsx @@ -578,7 +578,7 @@ function ArgumentReviewer(props: { }}> {buildArgOptionsString()} } {/* {props.onUnflatten !== undefined && } */} {props.onAddSubcommand !== undefined && checkCanAddSubcommand() && } - - } - - ) - } -} - - const WSEditorWrapper = (props: any) => { const params = useParams() diff --git a/src/web/src/views/workspace/WSEditorClientConfig.tsx b/src/web/src/views/workspace/WSEditorClientConfig.tsx new file mode 100644 index 00000000..877b595a --- /dev/null +++ b/src/web/src/views/workspace/WSEditorClientConfig.tsx @@ -0,0 +1,915 @@ +import * as React from 'react'; +import { Box, Dialog, DialogTitle, DialogContent, DialogActions, LinearProgress, Button, Paper, TextField, Alert, InputLabel, IconButton, Input, Typography, TypographyProps, Tabs, Tab } from '@mui/material'; +import axios from 'axios'; +import DoDisturbOnRoundedIcon from '@mui/icons-material/DoDisturbOnRounded'; +import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded'; +import { styled } from '@mui/system'; +import { Plane, Resource } from './WSEditorCommandContent'; +import { SwaggerItemSelector } from './WSEditorSwaggerPicker'; + +interface WSEditorClientConfigDialogProps { + workspaceUrl: string, + open: boolean, + onClose: (updated: boolean) => void +} + +interface WSEditorClientConfigDialogState { + updating: boolean, + invalidText: string | undefined, + isAdd: boolean, + + endpointType: "template" | "http-operation", + + templateAzureCloud: string, + templateAzureChinaCloud: string, + templateAzureUSGovernment: string, + templateAzureGermanCloud: string, + + aadAuthScopes: string[], + + planes: Plane[], + planeOptions: string[], + selectedPlane: string | null, + + moduleOptions: string[], + moduleOptionsCommonPrefix: string, + selectedModule: string | null, + + resourceProviderOptions: string[], + resourceProviderOptionsCommonPrefix: string, + selectedResourceProvider: string | null, + + versionOptions: string[], + versionResourceIdMap: SwaggerVersionResourceIdMap, + selectedVersion: string | null, + + resourceIdOptions: string[], + selectedResourceId: string | null, + subresource: string, +} + +interface SwaggerVersionResourceIdMap { + [version: string]: string[] +} + +interface ClientEndpointResource { + plane: string, + module: string, + version: string, + id: string, + subresource: string, +} + +const AuthTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.main, + fontFamily: "'Roboto Condensed', sans-serif", + fontSize: 16, + fontWeight: 400, +})); + +const MiddlePadding = styled(Box)(({ theme }) => ({ + height: '1.5vh' +})); + +class WSEditorClientConfigDialog extends React.Component { + + constructor(props: WSEditorClientConfigDialogProps) { + super(props); + this.state = { + updating: false, + invalidText: undefined, + isAdd: true, + + endpointType: "template", + + templateAzureCloud: "", + templateAzureChinaCloud: "", + templateAzureUSGovernment: "", + templateAzureGermanCloud: "", + + aadAuthScopes: ["",], + + planes: [], + planeOptions: [], + selectedPlane: null, + + moduleOptions: [], + moduleOptionsCommonPrefix: '', + selectedModule: null, + + resourceProviderOptions: [], + resourceProviderOptionsCommonPrefix: '', + selectedResourceProvider: null, + + versionOptions: [], + versionResourceIdMap: {}, + selectedVersion: null, + + resourceIdOptions: [], + selectedResourceId: null, + subresource: "", + + } + } + + componentDidMount(): void { + this.loadPlanes().then(async () => { + await this.loadWorkspaceClientConfig(); + const { selectedPlane, selectedModule, selectedResourceProvider, selectedVersion } = this.state; + await this.onPlaneSelectorUpdate(selectedPlane ?? this.state.planeOptions[0]); + if (selectedModule) { + await this.loadResourceProviders(selectedModule); + } + if (selectedResourceProvider) { + await this.loadResources(selectedResourceProvider, selectedVersion); + } + }); + } + + loadPlanes = async () => { + try { + this.setState({ + updating: true + }); + + let res = await axios.get(`/AAZ/Specs/Planes`); + const planes: Plane[] = res.data.map((v: any) => { + return { + name: v.name, + displayName: v.displayName, + moduleOptions: undefined, + } + }) + const planeOptions: string[] = res.data.map((v: any) => v.displayName) + this.setState({ + planes: planes, + planeOptions: planeOptions, + updating: false + }) + await this.onPlaneSelectorUpdate(planeOptions[0]); + } catch (err: any) { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ + updating: false, + invalidText: `ResponseError: ${data.message!}`, + }) + } + } + } + + onPlaneSelectorUpdate = async (planeDisplayName: string | null) => { + let plane = this.state.planes.find((v) => v.displayName === planeDisplayName) ?? null; + if (this.state.selectedPlane !== plane?.displayName ?? null) { + if (!plane) { + return + } + this.setState({ + selectedPlane: plane?.displayName ?? null, + }) + await this.loadSwaggerModules(plane); + } else { + this.setState({ + selectedPlane: plane?.displayName ?? null + }) + } + } + + loadSwaggerModules = async (plane: Plane | null) => { + if (plane !== null) { + if (plane!.moduleOptions?.length) { + this.setState({ + moduleOptions: plane!.moduleOptions!, + moduleOptionsCommonPrefix: `/Swagger/Specs/${plane!.name}/`, + }) + await this.onModuleSelectionUpdate(null); + } else { + try { + this.setState({ + updating: true + }); + let res = await axios.get(`/Swagger/Specs/${plane!.name}`); + const options: string[] = res.data.map((v: any) => (v.url)); + this.setState(preState => { + let planes = preState.planes; + let index = planes.findIndex((v) => v.name === plane!.name); + planes[index].moduleOptions = options; + return { + ...preState, + updating: false, + planes: planes, + moduleOptions: options, + moduleOptionsCommonPrefix: `/Swagger/Specs/${plane!.name}/`, + } + }) + await this.onModuleSelectionUpdate(null); + } catch (err: any) { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ + updating: false, + invalidText: `ResponseError: ${data.message!}`, + }) + } + } + } + } else { + this.setState({ + moduleOptions: [], + moduleOptionsCommonPrefix: '', + }) + await this.onModuleSelectionUpdate(null); + } + + } + + onModuleSelectionUpdate = async (moduleValueUrl: string | null) => { + if (this.state.selectedModule !== moduleValueUrl) { + this.setState({ + selectedModule: moduleValueUrl + }); + await this.loadResourceProviders(moduleValueUrl); + } else { + this.setState({ + selectedModule: moduleValueUrl + }) + } + } + + loadResourceProviders = async (moduleUrl: string | null) => { + if (moduleUrl !== null) { + try { + this.setState({ + updating: true + }); + let res = await axios.get(`${moduleUrl}/ResourceProviders`); + const options: string[] = res.data.map((v: any) => (v.url)); + let selectedResourceProvider = options.length === 1 ? options[0] : null; + this.setState({ + updating: false, + resourceProviderOptions: options, + resourceProviderOptionsCommonPrefix: `${moduleUrl}/ResourceProviders/`, + }); + this.onResourceProviderUpdate(selectedResourceProvider) + } catch (err: any) { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ + updating: false, + invalidText: `ResponseError: ${data.message!}`, + }) + } + } + } else { + this.setState({ + resourceProviderOptions: [], + resourceProviderOptionsCommonPrefix: '', + }) + this.onResourceProviderUpdate(null); + } + } + + onResourceProviderUpdate = async (resourceProviderUrl: string | null) => { + if (this.state.selectedResourceProvider !== resourceProviderUrl) { + this.setState({ + selectedResourceProvider: resourceProviderUrl, + }) + await this.loadResources(resourceProviderUrl, null); + } else { + this.setState({ + selectedResourceProvider: resourceProviderUrl + }) + } + } + + + loadResources = async (resourceProviderUrl: string | null, selectVersion: string | null) => { + if (resourceProviderUrl != null) { + this.setState({ + invalidText: undefined, + updating: true, + }) + try { + let res = await axios.get(`${resourceProviderUrl}/Resources`); + const versionResourceIdMap: SwaggerVersionResourceIdMap = {} + const versionOptions: string[] = [] + const resourceIdList: string[] = [] + res.data.forEach((resource: any) => { + resourceIdList.push(resource.id); + const resourceVersions = resource.versions.map((v: any) => v.version) + resourceVersions.forEach((v: any) => { + if (!(v in versionResourceIdMap)) { + versionResourceIdMap[v] = []; + versionOptions.push(v); + } + versionResourceIdMap[v].push(resource.id); + }) + }) + versionOptions.sort((a, b) => a.localeCompare(b)).reverse() + if (selectVersion === null && (versionOptions.length === 0 || versionOptions.findIndex(v => v === selectVersion) < 0)) { + selectVersion = null; + } + if (!selectVersion && versionOptions.length > 0) { + selectVersion = versionOptions[0]; + } + + this.setState({ + updating: false, + versionResourceIdMap: versionResourceIdMap, + versionOptions: versionOptions, + }) + this.onVersionUpdate(selectVersion); + } catch (err: any) { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ + invalidText: `ResponseError: ${data.message!}`, + }) + } + } + } else { + this.setState({ + versionOptions: [], + }) + this.onVersionUpdate(null) + } + } + + onVersionUpdate = (version: string | null) => { + this.setState(preState => { + let selectedResourceId = preState.selectedResourceId; + let resourceIdOptions: string[] = []; + if (version != null) { + resourceIdOptions = [...preState.versionResourceIdMap[version]] + .sort((a, b) => a.toString().localeCompare(b.toString())); + if (selectedResourceId !== null && resourceIdOptions.findIndex(v => v === selectedResourceId) < 0) { + selectedResourceId = null; + } + } + return { + ...preState, + resourceIdOptions: resourceIdOptions, + selectedVersion: version, + preferredAAZVersion: version, + selectedResourceId: selectedResourceId, + } + }) + } + + loadWorkspaceClientConfig = async () => { + this.setState({ updating: true }); + try { + let res = await axios.get(`${this.props.workspaceUrl}/ClientConfig`); + const clientConfig: ClientConfig = { + version: res.data.version, + auth: res.data.auth, + } + let templateAzureCloud = ""; + let templateAzureChinaCloud = ""; + let templateAzureUSGovernment = ""; + let templateAzureGermanCloud = ""; + let endpointType: "template" | "http-operation" = "template"; + let selectedPlane: string | null = null; + let selectedModule: string | null = null; + let selectedResourceProvider: string | null = null; + let selectedVersion: string | null = null; + let selectedResourceId: string | null = null; + let subresource: string = ""; + + if (res.data.endpoints.type === "template") { + clientConfig.endpointTemplates = {}; + res.data.endpoints.templates.forEach((value: any) => { + clientConfig.endpointTemplates![value.cloud] = value.template; + }); + + endpointType = "template"; + templateAzureCloud = clientConfig.endpointTemplates!['AzureCloud'] ?? ""; + templateAzureChinaCloud = clientConfig.endpointTemplates!['AzureChinaCloud'] ?? ""; + templateAzureUSGovernment = clientConfig.endpointTemplates!['AzureUSGovernment'] ?? ""; + templateAzureGermanCloud = clientConfig.endpointTemplates!['AzureGermanCloud'] ?? ""; + } else if (res.data.endpoints.type === "http-operation") { + clientConfig.endpointResource = res.data.endpoints.resource; + let rpUrl: string = clientConfig.endpointResource!.swagger.split('/Paths/')[0]; + let moduleUrl: string = rpUrl.split('/ResourceProviders/')[0]; + let planeUrl: string = moduleUrl.split('/')[0]; + selectedResourceProvider = `/Swagger/Specs/${rpUrl}` + selectedModule = `/Swagger/Specs/${moduleUrl}` + selectedPlane = `/Swagger/Specs/${planeUrl}` + selectedVersion = clientConfig.endpointResource!.version; + selectedResourceId = clientConfig.endpointResource!.id; + subresource = clientConfig.endpointResource!.subresource ?? ''; + endpointType = "http-operation"; + } + + this.setState({ + aadAuthScopes: clientConfig.auth.aad.scopes ?? ["",], + endpointType: endpointType, + templateAzureCloud: templateAzureCloud, + templateAzureChinaCloud: templateAzureChinaCloud, + templateAzureUSGovernment: templateAzureUSGovernment, + templateAzureGermanCloud: templateAzureGermanCloud, + selectedPlane: selectedPlane, + selectedModule: selectedModule, + selectedResourceProvider: selectedResourceProvider, + selectedVersion: selectedVersion, + selectedResourceId: selectedResourceId, + subresource: subresource, + isAdd: false + }); + } catch (err: any) { + // catch 404 error + if (err.response?.status === 404) { + this.setState({ + isAdd: true, + }); + } else { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ invalidText: `ResponseError: ${data.message!}: ${JSON.stringify(data.details)}` }); + } + } + } + + this.setState({ updating: false }); + } + + handleClose = () => { + this.props.onClose(false); + } + + handleUpdate = async () => { + let { aadAuthScopes, endpointType} = this.state + let templates: ClientEndpointTemplate[] | undefined = undefined; + let resource: ClientEndpointResource | undefined = undefined; + + if (endpointType === "template") { + let { templateAzureCloud, templateAzureChinaCloud, templateAzureGermanCloud, templateAzureUSGovernment } = this.state + templateAzureCloud = templateAzureCloud.trim(); + if (templateAzureCloud.length < 1) { + this.setState({ + invalidText: "Azure Cloud Endpoint Template is required." + }); + return; + } + templateAzureChinaCloud = templateAzureChinaCloud.trim(); + templateAzureUSGovernment = templateAzureUSGovernment.trim(); + templateAzureGermanCloud = templateAzureGermanCloud.trim(); + // verify template url using regex, like https://{vaultName}.vault.azure.net + const templateRegex = /^https:\/\/((\{[a-zA-Z0-9]+\})|([^{}.]+))(.((\{[a-zA-Z0-9]+\})|([^{}.]+)))*(\/)?$/; + if (!templateRegex.test(templateAzureCloud)) { + this.setState({ + invalidText: "Azure Cloud Endpoint Template is invalid." + }); + return; + } + + if (templateAzureChinaCloud.length > 0 && !templateRegex.test(templateAzureChinaCloud)) { + this.setState({ + invalidText: "Azure China Cloud Endpoint Template is invalid." + }); + return; + } + + if (templateAzureUSGovernment.length > 0 && !templateRegex.test(templateAzureUSGovernment)) { + this.setState({ + invalidText: "Azure US Government Endpoint Template is invalid." + }); + return; + } + + if (templateAzureGermanCloud.length > 0 && !templateRegex.test(templateAzureGermanCloud)) { + this.setState({ + invalidText: "Azure German Cloud Endpoint Template is invalid." + }); + return; + } + + templates = [ + { cloud: 'AzureCloud', template: templateAzureCloud }, + ]; + if (templateAzureChinaCloud.length > 0) { + templates.push({ cloud: 'AzureChinaCloud', template: templateAzureChinaCloud }); + } + if (templateAzureUSGovernment.length > 0) { + templates.push({ cloud: 'AzureUSGovernment', template: templateAzureUSGovernment }); + } + if (templateAzureGermanCloud.length > 0) { + templates.push({ cloud: 'AzureGermanCloud', template: templateAzureGermanCloud }); + } + } else if (endpointType === "http-operation") { + + let {selectedPlane, selectedModule, selectedResourceProvider, selectedVersion, selectedResourceId, subresource, moduleOptionsCommonPrefix} = this.state; + if (!selectedPlane) { + this.setState({ + invalidText: "Plane is required." + }); + return; + } + if (!selectedModule) { + this.setState({ + invalidText: "Module is required." + }); + return; + } + if (!selectedResourceProvider) { + this.setState({ + invalidText: "Resource Provider is required." + }); + return; + } + if (!selectedVersion) { + this.setState({ + invalidText: "API Version is required." + }); + return; + } + if (!selectedResourceId) { + this.setState({ + invalidText: "Resource ID is required." + }); + return; + } + subresource = subresource.trim(); + if (subresource.length < 1) { + this.setState({ + invalidText: "Endpoint Property Index is required." + }); + return; + } + + resource = { + plane: selectedPlane.replace('/Swagger/Specs/', ''), + module: selectedModule.replace(moduleOptionsCommonPrefix, ''), + version: selectedVersion, + id: selectedResourceId, + subresource: subresource, + } + } + + aadAuthScopes = aadAuthScopes.map(scope => scope.trim()).filter(scope => scope.length > 0); + if (aadAuthScopes.length < 1) { + this.setState({ + invalidText: "AAD Auth Scopes is required." + }); + return; + } + + let auth = { + aad: { + scopes: aadAuthScopes, + } + } + + this.onUpdateClientConfig( + templates, + resource, + auth, + ); + } + + onUpdateClientConfig = async ( + templates: ClientEndpointTemplate[] | undefined, + resource: ClientEndpointResource | undefined, + auth: ClientAuth, + ) => { + this.setState({ updating: true }); + try { + await axios.post(`${this.props.workspaceUrl}/ClientConfig`, { + templates: templates, + resource: resource, + auth: auth, + }); + this.setState({ updating: false }); + this.props.onClose(true); + } catch (err: any) { + console.error(err.response); + if (err.response?.data?.message) { + const data = err.response!.data!; + this.setState({ invalidText: `ResponseError: ${data.message!}: ${JSON.stringify(data.details)}` }); + } + this.setState({ updating: false }); + } + + } + + onRemoveAadScope = (idx: number) => { + this.setState(preState => { + let aadAuthScopes: string[] = [...preState.aadAuthScopes.slice(0, idx), ...preState.aadAuthScopes.slice(idx + 1)]; + if (aadAuthScopes.length === 0) { + aadAuthScopes.push(""); + } + return { + ...preState, + aadAuthScopes: aadAuthScopes, + } + }) + } + + onModifyAadScope = (scope: string, idx: number) => { + this.setState(preState => { + return { + ...preState, + aadAuthScopes: [...preState.aadAuthScopes.slice(0, idx), scope, ...preState.aadAuthScopes.slice(idx + 1)] + } + }) + } + + onAddAadScope = () => { + this.setState(preState => { + return { + ...preState, + aadAuthScopes: [...preState.aadAuthScopes, ""], + } + }) + } + + buildAadScopeInput = (scope: string, idx: number) => { + return ( + + this.onRemoveAadScope(idx)} + aria-label='remove' + > + + + { + this.onModifyAadScope(event.target.value, idx); + }} + sx={{ flexGrow: 1 }} + placeholder="Input aad auth Scope here, e.g. https://metrics.monitor.azure.com/.default" + /> + + ) + } + + render() { + const { invalidText, updating, isAdd, aadAuthScopes, endpointType, templateAzureCloud, templateAzureChinaCloud, templateAzureUSGovernment, templateAzureGermanCloud } = this.state; + const { selectedModule, selectedResourceProvider, selectedVersion, selectedResourceId, subresource } = this.state; + return ( + + {isAdd ? "Setup Client Config" : "Modify Client Config"} + + {invalidText && {invalidText} } + Endpoint + + + { + this.setState({ + endpointType: newValue, + }) + }}> + + + + + {endpointType === "template" && + { + this.setState({ + templateAzureCloud: event.target.value, + }) + }} + margin='dense' + required + /> + + { + this.setState({ + templateAzureChinaCloud: event.target.value, + }) + }} + margin='normal' + /> + + { + this.setState({ + templateAzureUSGovernment: event.target.value, + }) + }} + margin='normal' + /> + + { + this.setState({ + templateAzureGermanCloud: event.target.value, + }) + }} + margin='normal' + /> + } + {endpointType === "http-operation" && + + + + + + + { + this.setState({ + selectedResourceId: resourceId, + }) + }} + /> + + { + this.setState({ + subresource: event.target.value, + }) + }} + margin='dense' + required + /> + + } + + + AAD Auth Scopes + {aadAuthScopes?.map(this.buildAadScopeInput)} + + + + + One more scope + + + + {updating && + + + + } + {!updating && + {!isAdd && } + + } + + ) + } +} + + +interface ClientEndpointTemplate { + cloud: string, + template: string, +} + +interface ClientTemplateMap { + [cloud: string]: string +} + +// interface ClientEndpointResource { +// version: string, +// id: string, +// subresource: string, +// // plane: string, +// // module: string, +// } + +interface ClientAADAuth { + scopes: string[], +} + +interface ClientAuth { + aad: ClientAADAuth, +} + +interface ClientConfig { + version: string, + endpointTemplates?: ClientTemplateMap, + endpointResource?: Resource, + auth: ClientAuth, +} + +export default WSEditorClientConfigDialog; +export type { ClientEndpointTemplate, ClientTemplateMap, ClientAADAuth, ClientConfig }; diff --git a/src/web/src/views/workspace/WSEditorCommandContent.tsx b/src/web/src/views/workspace/WSEditorCommandContent.tsx index 01f19353..ba410d79 100644 --- a/src/web/src/views/workspace/WSEditorCommandContent.tsx +++ b/src/web/src/views/workspace/WSEditorCommandContent.tsx @@ -11,6 +11,13 @@ import LabelIcon from '@mui/icons-material/Label'; import WSEditorCommandArgumentsContent, { ClsArgDefinitionMap, CMDArg, DecodeArgs } from './WSEditorCommandArgumentsContent'; import EditIcon from '@mui/icons-material/Edit'; + +interface Plane { + name: string, + displayName: string, + moduleOptions?: string[], +} + interface Example { name: string, commands: string[], @@ -1351,5 +1358,5 @@ const DecodeResponseClientConfig = (clientConfig: any): ClientConfig => { export default WSEditorCommandContent; export { DecodeResponseCommand }; -export type { Command, Resource, ResponseCommand, ResponseCommands }; +export type { Plane, Command, Resource, ResponseCommand, ResponseCommands }; diff --git a/src/web/src/views/workspace/WorkspaceSelector.tsx b/src/web/src/views/workspace/WorkspaceSelector.tsx index 813f539a..d8c7fc2e 100644 --- a/src/web/src/views/workspace/WorkspaceSelector.tsx +++ b/src/web/src/views/workspace/WorkspaceSelector.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { Url } from 'url'; import { SwaggerItemSelector } from './WSEditorSwaggerPicker'; import styled from '@emotion/styled'; +import { Plane } from './WSEditorCommandContent'; interface Workspace { @@ -168,12 +169,6 @@ class WorkspaceSelector extends React.Component Date: Thu, 23 Nov 2023 14:36:49 +0800 Subject: [PATCH 8/9] Fix some bugs --- .../command/model/configuration/_client.py | 2 + .../command/model/configuration/_http.py | 2 +- .../command/tests/api_tests/test_editor.py | 46 +++++++++++++++++++ .../views/workspace/WSEditorClientConfig.tsx | 31 +++++++++---- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/aaz_dev/command/model/configuration/_client.py b/src/aaz_dev/command/model/configuration/_client.py index 9794de53..2afc2cb1 100644 --- a/src/aaz_dev/command/model/configuration/_client.py +++ b/src/aaz_dev/command/model/configuration/_client.py @@ -323,6 +323,8 @@ def generate_args(self, ref_args): arguments, has_subresource=has_subresource, operation_id=self.operation.operation_id) def diff(self, old, level): + if type(self) is not type(old): + return f"Type: {type(old)} != {type(self)}" diff = {} if resource_diff := self.resource.diff(old.resource, level): diff["resource"] = resource_diff diff --git a/src/aaz_dev/command/model/configuration/_http.py b/src/aaz_dev/command/model/configuration/_http.py index bbb6d8ce..c4085c36 100644 --- a/src/aaz_dev/command/model/configuration/_http.py +++ b/src/aaz_dev/command/model/configuration/_http.py @@ -319,7 +319,7 @@ class Options: def diff(self, old, level): diff = {} if level >= CMDDiffLevelEnum.BreakingChange: - if (self.status_codes is not None) != (old.status_codes is not None): + if (not self.status_codes) != (not old.status_codes): diff["status_codes"] = f"{old.status_codes} != {self.status_codes}" elif self.status_codes and set(self.status_codes) != set(old.status_codes): diff["status_codes"] = f"{old.status_codes} != {self.status_codes}" diff --git a/src/aaz_dev/command/tests/api_tests/test_editor.py b/src/aaz_dev/command/tests/api_tests/test_editor.py index d1376589..24e34ab9 100644 --- a/src/aaz_dev/command/tests/api_tests/test_editor.py +++ b/src/aaz_dev/command/tests/api_tests/test_editor.py @@ -2482,3 +2482,49 @@ def test_dataplane_attestation(self, ws_name): rv = c.post(f"{ws_url}/Generate") self.assertTrue(rv.status_code == 200) + # update client config without change + rv = c.get(f"{ws_url}/ClientConfig") + self.assertTrue(rv.status_code == 200) + client_config = rv.get_json() + old_version = client_config['version'] + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01", + "subresource": "properties.attestUri", + }, + }) + self.assertTrue(rv.status_code == 200) + rv = c.get(f"{ws_url}/ClientConfig") + self.assertTrue(rv.status_code == 200) + client_config = rv.get_json() + self.assertTrue(client_config['version'] == old_version) + + # update client config by api version changed. + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01-preview", + "subresource": "properties.attestUri", + }, + }) + self.assertTrue(rv.status_code == 200) + client_config = rv.get_json() + # the client version should be updated. + self.assertTrue(client_config['version'] != old_version) \ No newline at end of file diff --git a/src/web/src/views/workspace/WSEditorClientConfig.tsx b/src/web/src/views/workspace/WSEditorClientConfig.tsx index 877b595a..e5b35396 100644 --- a/src/web/src/views/workspace/WSEditorClientConfig.tsx +++ b/src/web/src/views/workspace/WSEditorClientConfig.tsx @@ -299,7 +299,14 @@ class WSEditorClientConfigDialog extends React.Component { resourceIdList.push(resource.id); - const resourceVersions = resource.versions.map((v: any) => v.version) + const resourceVersions = resource.versions.filter((v: ResourceVersion) => { + for (let key in v.operations) { + if (v.operations[key].toUpperCase() === 'GET') { + return true; + } + } + return false; + }).map((v: any) => v.version) resourceVersions.forEach((v: any) => { if (!(v in versionResourceIdMap)) { versionResourceIdMap[v] = []; @@ -829,7 +836,7 @@ class WSEditorClientConfigDialog extends React.Component { this.setState({ @@ -888,14 +895,6 @@ interface ClientTemplateMap { [cloud: string]: string } -// interface ClientEndpointResource { -// version: string, -// id: string, -// subresource: string, -// // plane: string, -// // module: string, -// } - interface ClientAADAuth { scopes: string[], } @@ -911,5 +910,17 @@ interface ClientConfig { auth: ClientAuth, } +type ResourceVersion = { + version: string + operations: ResourceVersionOperations + file: string + id: string + path: string +} + +type ResourceVersionOperations = { + [Named: string]: string +} + export default WSEditorClientConfigDialog; export type { ClientEndpointTemplate, ClientTemplateMap, ClientAADAuth, ClientConfig }; From 417ea1a915159e7b61a17c0630ad8b0fa6622240 Mon Sep 17 00:00:00 2001 From: Kai Ru Date: Thu, 23 Nov 2023 15:11:17 +0800 Subject: [PATCH 9/9] Raise error when index in invalid --- .../controller/workspace_cfg_editor.py | 97 +++++++++++-------- .../command/tests/api_tests/test_editor.py | 57 ++++++++++- 2 files changed, 113 insertions(+), 41 deletions(-) diff --git a/src/aaz_dev/command/controller/workspace_cfg_editor.py b/src/aaz_dev/command/controller/workspace_cfg_editor.py index 1fd9857b..de0bca07 100644 --- a/src/aaz_dev/command/controller/workspace_cfg_editor.py +++ b/src/aaz_dev/command/controller/workspace_cfg_editor.py @@ -1329,7 +1329,7 @@ def _build_simple_index_base(cls, schema, idx, index=None, **kwargs): raise NotImplementedError(f"Not support schema '{type(schema)}'") index = CMDSimpleIndexBase() if idx: - raise exceptions.InvalidAPIUsage("Simple schema is not support feature index") + raise exceptions.InvalidAPIUsage(f"Not support remain index {idx}") return index @classmethod @@ -1344,45 +1344,53 @@ def _build_object_index_base(cls, schema, idx, index=None, prune=False, **kwargs current_idx = idx[0] remain_idx = idx[1:] + find_idx = False if schema.props: for prop in schema.props: - if prop.name == current_idx: - if prune: - assert isinstance(index, CMDSelectorIndex) - name = f"{index.name}.{prop.name}" - if isinstance(prop, CMDClsSchema): - prop = prop.implement - # ignore the current index, return the sub index with index.name prefix - if isinstance(prop, CMDObjectSchema): - return cls._build_object_index(prop, remain_idx, name=name, **kwargs) - elif isinstance(prop, CMDArraySchema): - return cls._build_array_index(prop, remain_idx, name=name, **kwargs) - else: - return cls._build_simple_index(prop, remain_idx, name=name, **kwargs) + if prop.name != current_idx: + continue + find_idx = True + if prune: + assert isinstance(index, CMDSelectorIndex) + name = f"{index.name}.{prop.name}" + if isinstance(prop, CMDClsSchema): + prop = prop.implement + # ignore the current index, return the sub index with index.name prefix + if isinstance(prop, CMDObjectSchema): + return cls._build_object_index(prop, remain_idx, name=name, **kwargs) + elif isinstance(prop, CMDArraySchema): + return cls._build_array_index(prop, remain_idx, name=name, **kwargs) else: - name = prop.name - if isinstance(prop, CMDClsSchema): - prop = prop.implement - if isinstance(prop, CMDObjectSchema): - index.prop = cls._build_object_index(prop, remain_idx, name=name, **kwargs) - break - elif isinstance(prop, CMDArraySchema): - index.prop = cls._build_array_index(prop, remain_idx, name=name, **kwargs) - break - else: - index.prop = cls._build_simple_index(prop, remain_idx, name=name, **kwargs) - break + return cls._build_simple_index(prop, remain_idx, name=name, **kwargs) + else: + name = prop.name + if isinstance(prop, CMDClsSchema): + prop = prop.implement + if isinstance(prop, CMDObjectSchema): + index.prop = cls._build_object_index(prop, remain_idx, name=name, **kwargs) + break + elif isinstance(prop, CMDArraySchema): + index.prop = cls._build_array_index(prop, remain_idx, name=name, **kwargs) + break + else: + index.prop = cls._build_simple_index(prop, remain_idx, name=name, **kwargs) + break if schema.discriminators: for disc in schema.discriminators: if disc.get_safe_value() == current_idx: + find_idx = True index.discriminator = cls._build_object_index_discriminator(disc, remain_idx, **kwargs) break if schema.additional_props and current_idx == "{}": + find_idx = True index.additional_props = cls._build_object_index_additional_prop(schema.additional_props, remain_idx, **kwargs) + if not find_idx: + raise exceptions.InvalidAPIUsage(f"Cannot find remain index {idx}") + return index @classmethod @@ -1397,7 +1405,8 @@ def _build_array_index_base(cls, schema, idx, index=None, **kwargs): current_idx = idx[0] remain_idx = idx[1:] - assert current_idx == '[]' + if current_idx != '[]': + raise exceptions.InvalidAPIUsage(f"Cannot find index '{idx}'") item = schema.item if isinstance(item, CMDClsSchemaBase): @@ -1473,29 +1482,37 @@ def _build_object_index_discriminator(cls, schema, idx, **kwargs): current_idx = idx[0] remain_idx = idx[1:] + find_idx = False + if schema.props: for prop in schema.props: - if prop.name == current_idx: - name = prop.name - if isinstance(prop, CMDClsSchema): - prop = prop.implement + if prop.name != current_idx: + continue + find_idx = True + name = prop.name + if isinstance(prop, CMDClsSchema): + prop = prop.implement - if isinstance(prop, CMDObjectSchema): - index.prop = cls._build_object_index(prop, remain_idx, name, **kwargs) - break - elif isinstance(prop, CMDArraySchema): - index.prop = cls._build_array_index(prop, remain_idx, name, **kwargs) - break - else: - index.prop = cls._build_simple_index(prop, remain_idx, name, **kwargs) - break + if isinstance(prop, CMDObjectSchema): + index.prop = cls._build_object_index(prop, remain_idx, name, **kwargs) + break + elif isinstance(prop, CMDArraySchema): + index.prop = cls._build_array_index(prop, remain_idx, name, **kwargs) + break + else: + index.prop = cls._build_simple_index(prop, remain_idx, name, **kwargs) + break if schema.discriminators: for disc in schema.discriminators: if disc.get_safe_value() == current_idx: + find_idx = True index.discriminator = cls._build_object_index_discriminator(disc, remain_idx, **kwargs) break + if not find_idx: + raise exceptions.InvalidAPIUsage(f"Cannot find remain index {idx}") + return index @classmethod diff --git a/src/aaz_dev/command/tests/api_tests/test_editor.py b/src/aaz_dev/command/tests/api_tests/test_editor.py index 24e34ab9..d5a75eb1 100644 --- a/src/aaz_dev/command/tests/api_tests/test_editor.py +++ b/src/aaz_dev/command/tests/api_tests/test_editor.py @@ -2527,4 +2527,59 @@ def test_dataplane_attestation(self, ws_name): self.assertTrue(rv.status_code == 200) client_config = rv.get_json() # the client version should be updated. - self.assertTrue(client_config['version'] != old_version) \ No newline at end of file + self.assertTrue(client_config['version'] != old_version) + + # update client config with invalid subresource + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01-preview", + "subresource": "property.attestUri", + }, + }) + self.assertTrue(rv.status_code == 400) + self.assertEqual(rv.json['message'], "Cannot find remain index ['property', 'attestUri']") + + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01-preview", + "subresource": "properties.attest", + }, + }) + self.assertTrue(rv.status_code == 400) + self.assertEqual(rv.json['message'], "Cannot find remain index ['attest']") + + rv = c.post(f"{ws_url}/ClientConfig", json={ + "auth": { + "aad": { + "scopes": ["https://attest.azure.net/.default"] + } + }, + "resource": { + "plane": PlaneEnum.Mgmt, + "module": module, + "id": swagger_resource_path_to_resource_id( + '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Attestation/attestationProviders/{providerName}'), + "version": "2021-06-01-preview", + "subresource": "properties.attestUri.invalid", + }, + }) + self.assertTrue(rv.status_code == 400) + self.assertEqual(rv.json['message'], "Not support remain index ['invalid']")