Skip to content

Commit

Permalink
Merge pull request #311 from Azure/feature-data-plane-support
Browse files Browse the repository at this point in the history
Support data plane client with dynamic endpoint fetched from a response.
  • Loading branch information
kairu-ms authored Nov 23, 2023
2 parents d469845 + 81a5c10 commit 26e6e59
Show file tree
Hide file tree
Showing 48 changed files with 3,148 additions and 848 deletions.
5 changes: 3 additions & 2 deletions src/aaz_dev/cli/controller/az_atomic_profile_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 78 additions & 4 deletions src/aaz_dev/cli/controller/az_client_generator.py
Original file line number Diff line number Diff line change
@@ -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')

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

109 changes: 109 additions & 0 deletions src/aaz_dev/cli/controller/az_command_ctx.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 26e6e59

Please sign in to comment.