Skip to content

Commit bdb091b

Browse files
committed
implement to retrieve autorest configuration in modules
1 parent 63eff50 commit bdb091b

File tree

8 files changed

+295
-20
lines changed

8 files changed

+295
-20
lines changed

src/aaz_dev/app/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ def invalid_api_usage(e):
2727
return jsonify(e.to_dict()), e.status_code
2828

2929
# register url converters
30-
from .url_converters import Base64Converter, NameConverter, NameWithCapitalConverter, NamesPathConverter, ListPathConvertor
30+
from .url_converters import Base64Converter, NameConverter, NameWithCapitalConverter, NamesPathConverter, ListPathConvertor, PSNamesPathConverter
3131
app.url_map.converters['base64'] = Base64Converter
3232
app.url_map.converters['name'] = NameConverter
3333
app.url_map.converters['Name'] = NameWithCapitalConverter
3434
app.url_map.converters['names_path'] = NamesPathConverter
3535
app.url_map.converters['list_path'] = ListPathConvertor
36+
app.url_map.converters['PSNamesPath'] = PSNamesPathConverter
3637

3738
# register routes of swagger module
3839
from swagger.api import register_blueprints

src/aaz_dev/app/url_converters.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def to_url(self, values):
3232
return super(NamesPathConverter, self).to_url(values)
3333
return '/'.join(super(NamesPathConverter, self).to_url(value) for value in values)
3434

35-
3635
class ListPathConvertor(PathConverter):
3736

3837
def to_python(self, value):
@@ -43,5 +42,17 @@ def to_url(self, values):
4342
return super(ListPathConvertor, self).to_url(values)
4443
return '/'.join(super(ListPathConvertor, self).to_url(value) for value in values)
4544

45+
class PSNamesPathConverter(PathConverter):
46+
regex = r"([A-Z][a-zA-Z0-9]*)/([A-Z][a-zA-Z0-9]*\.Autorest)"
47+
weight = 200
48+
49+
def to_python(self, value):
50+
return value.split('/')
51+
52+
def to_url(self, values):
53+
if isinstance(values, str):
54+
return super(PSNamesPathConverter, self).to_url(values)
55+
return '/'.join(super(PSNamesPathConverter, self).to_url(value) for value in values)
56+
4657

47-
__all__ = ["Base64Converter", "NameConverter", "NamesPathConverter", "ListPathConvertor"]
58+
__all__ = ["Base64Converter", "NameConverter", "NamesPathConverter", "ListPathConvertor", "PSNamesPathConverter"]

src/aaz_dev/ps/api/powershell.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,65 @@
22

33
from utils.config import Config
44
from utils import exceptions
5-
from command.controller.specs_manager import AAZSpecsManager
5+
from ps.controller.ps_module_manager import PSModuleManager
6+
from app.url_converters import PSNamesPathConverter
7+
# from command.controller.specs_manager import AAZSpecsManager
68
import logging
9+
import re
710

811
logging.basicConfig(level="INFO")
912

1013

1114
bp = Blueprint('powershell', __name__, url_prefix='/PS/Powershell')
1215

1316

14-
@bp.route("/Path", methods=("GET", "PUT"))
17+
@bp.route("/Path", methods=("GET", ))
1518
def powershell_path():
19+
if Config.POWERSHELL_PATH is None:
20+
raise exceptions.InvalidAPIUsage("PowerShell path is not set, please add `--ps` option to `aaz-dev run` command or set up `AAZ_POWERSHELL_PATH` environment variable")
21+
return jsonify({"path": Config.POWERSHELL_PATH})
22+
23+
24+
@bp.route("/Modules", methods=("GET", "POST"))
25+
def powershell_modules():
26+
manager = PSModuleManager()
27+
if request.method == "GET":
28+
modules = manager.list_modules()
29+
result = []
30+
for module in modules:
31+
result.append({
32+
**module,
33+
'url': url_for('powershell.powershell_module', module_names=module['name']),
34+
})
35+
return jsonify(result)
36+
elif request.method == "POST":
37+
# create a new module in powershell
38+
data = request.get_json()
39+
if not data or not isinstance(data, dict) or 'name' not in data:
40+
raise exceptions.InvalidAPIUsage("Invalid request body")
41+
if not re.match(PSNamesPathConverter.regex, data['name'].split('/')):
42+
raise exceptions.InvalidAPIUsage("Invalid module name")
43+
module_names = data['name'].split('/')
44+
# make sure the name is follow the PSNamesPathConverter.regex
45+
module = manager.create_new_mod(module_names)
46+
result = module.to_primitive()
47+
result['url'] = url_for('powershell.powershell_module', module_names=module.name)
48+
else:
49+
raise NotImplementedError()
50+
return jsonify(result)
51+
52+
53+
@bp.route("/Modules/<PSNamesPath:module_names>", methods=("GET", "PUT", "PATCH"))
54+
def powershell_module(module_names):
55+
manager = PSModuleManager()
1656
if request.method == "GET":
17-
return jsonify({"path": Config.POWERSHELL_PATH})
57+
result = manager.load_module(module_names)
58+
# result = module.to_primitive()
59+
result['url'] = url_for('powershell.powershell_module', module_names=result['name'])
1860
elif request.method == "PUT":
19-
data = request.json
20-
try:
21-
Config.validate_and_setup_powershell_path(None, None, data["path"])
22-
except ValueError as e:
23-
raise exceptions.InvalidAPIUsage(str(e))
24-
return jsonify({"path": Config.POWERSHELL_PATH})
61+
raise NotImplementedError()
62+
elif request.method == "PATCH":
63+
raise NotImplementedError()
64+
else:
65+
raise NotImplementedError()
66+
return jsonify(result)
Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,94 @@
1-
import ast
2-
import glob
3-
import json
41
import logging
52
import os
6-
import pkgutil
7-
import re
3+
import yaml
84

9-
from utils import exceptions
105
from utils.config import Config
11-
from collections import deque
126

137
logger = logging.getLogger('backend')
148

159

1610
class PSModuleManager:
17-
pass
11+
12+
def __init__(self):
13+
module_folder = self._find_module_folder()
14+
self.folder = module_folder
15+
16+
def _find_module_folder(self):
17+
powershell_folder = Config.POWERSHELL_PATH
18+
if not os.path.exists(powershell_folder) or not os.path.isdir(powershell_folder):
19+
raise ValueError(f"Invalid PowerShell folder: '{powershell_folder}'")
20+
module_folder = os.path.join(powershell_folder, "src")
21+
if not os.path.exists(module_folder):
22+
raise ValueError(f"Invalid PowerShell folder: cannot find modules in: '{module_folder}'")
23+
return module_folder
24+
25+
def list_modules(self):
26+
modules = []
27+
for folder_name in os.listdir(self.folder):
28+
path = os.path.join(self.folder, folder_name)
29+
if os.path.isdir(path):
30+
for sub_folder in os.listdir(path):
31+
if os.path.isdir(os.path.join(path, sub_folder)) and sub_folder.endswith(".Autorest"):
32+
name = f"{folder_name}/{sub_folder}"
33+
modules.append({
34+
"name": name,
35+
"folder": os.path.join(path, sub_folder)
36+
})
37+
return sorted(modules, key=lambda a: a['name'])
38+
39+
def create_new_mod(self, module_names):
40+
if isinstance(module_names, str):
41+
module_names = module_names.split('/')
42+
folder = os.path.join(self.folder, *module_names)
43+
os.makedirs(folder, exist_ok=True)
44+
45+
def load_module(self, module_names):
46+
if isinstance(module_names, str):
47+
module_names = module_names.split('/')
48+
folder = os.path.join(self.folder, *module_names)
49+
if not os.path.exists(folder):
50+
raise ValueError(f"Module folder not found: '{folder}'")
51+
autorest_config = self.load_autorest_config(module_names)
52+
return {
53+
**autorest_config,
54+
"name": "/".join(module_names),
55+
"folder": folder
56+
}
57+
58+
def load_autorest_config(self, module_names):
59+
if isinstance(module_names, str):
60+
module_names = module_names.split('/')
61+
folder = os.path.join(self.folder, *module_names)
62+
readme_file = os.path.join(folder, "README.md")
63+
if not os.path.exists(readme_file):
64+
raise ValueError(f"README.md not found in: '{readme_file}'")
65+
with open(readme_file, "r") as f:
66+
content = f.readlines()
67+
autorest_config = []
68+
in_autorest_config_section = False
69+
in_yaml_section = False
70+
for line in content:
71+
if line.strip().startswith("### AutoRest Configuration"):
72+
in_autorest_config_section = True
73+
elif in_autorest_config_section:
74+
if line.strip().startswith("###"):
75+
break
76+
if line.strip().startswith("```") and 'yaml' in line:
77+
in_yaml_section = True
78+
elif in_yaml_section:
79+
if line.strip().startswith("```"):
80+
in_yaml_section = False
81+
else:
82+
if line.strip():
83+
autorest_config.append(line)
84+
else:
85+
autorest_config.append("")
86+
autorest_config_raw = "\n".join(autorest_config)
87+
try:
88+
yaml_config = yaml.load(autorest_config_raw, Loader=yaml.FullLoader)
89+
except Exception as e:
90+
raise ValueError(f"Failed to parse autorest config: {e} for readme_file: {readme_file}")
91+
return {
92+
"autorest_config": yaml_config,
93+
# "raw": autorest_config_raw # can be used for directive merging
94+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
metadata: # display to the users
2+
dict:
3+
authors: Microsoft Corporation
4+
companyName: Microsoft Corporation
5+
copyright: Microsoft Corporation. All rights reserved.
6+
description: 'Microsoft Azure PowerShell: Portal Dashboard cmdlets'
7+
licenseUri: https://aka.ms/azps-license
8+
owners: Microsoft Corporation
9+
projectUri: https://github.com/Azure/azure-powershell
10+
releaseNotes: Initial release of Portal Dashboard cmdlets.
11+
requireLicenseAcceptance: true
12+
scriptsToProcess:
13+
- ./custom/Helpers.ps1
14+
tags: Azure ResourceManager ARM PSModule Portal Dashboard
15+
prefix: # the variable
16+
basic:
17+
- Az # the default value
18+
service-name: # by default it's calculated
19+
basic:
20+
- ADDomainServices
21+
- CostManagement
22+
module-name:
23+
basic:
24+
- $(prefix).$(service-name) # the default value
25+
- Az.SignalR
26+
root-module-name: # used for sub module to generate the code in root module if there are multiple sub modules
27+
basic:
28+
- $(prefix).AlertsManagement
29+
- $(prefix).AppConfiguration
30+
namespace: # used for sub module to define the powershell class namespace
31+
basic:
32+
- Microsoft.Azure.PowerShell.Cmdlets.$(service-name) # the default value
33+
- Microsoft.Azure.PowerShell.Cmdlets.Metric
34+
subject-prefix: # the subject prefix is used in the commandlet name, which will be added after {action}-AZ{subprefix}
35+
basic:
36+
- '' # no subject prefix included
37+
- $(service-name) # the default value
38+
- $Informatica
39+
- $elastic
40+
- AD
41+
repo: # the variable, used to find the required files, it's used for input-files
42+
basic:
43+
- https://github.com/Azure/azure-rest-api-specs/tree/$(commit) # the default value, and also support for other repos
44+
require: # required readme.md files for autorest consume
45+
list:
46+
- $(this-folder)/../../readme.azure.noprofile.md # always needed
47+
- $(repo)/specification/appcomplianceautomation/resource-manager/readme.md
48+
- $(repo)/specification/azurefleet/resource-manager/readme.md
49+
- $(this-folder)/../../helpers/KeyVault/readme.noprofile.md
50+
- $(this-folder)/../../helpers/ManagedIdentity/readme.noprofile.md
51+
endpoint-resource-id-key-name: # data-plane used, the resource id key name should be registered in azure powershell account before. It will control the client endpoint and auth.
52+
inlining-threshold: # flatten threshold, 40, which used to control the max allowed flatten properties into outer layer
53+
sanitize-names: # TODO: need code for detail how to check the sanitize rules for the argument names
54+
basic:
55+
- true # the default value
56+
support-json-input: # default value is true
57+
tag: # used to filter out the input apis, the tag should be in the swagger readme.md file
58+
basic:
59+
- package-2022-10-12-preview
60+
- package-2023-01-01
61+
try-require: # using the directive configuration if it exists as prefix
62+
list:
63+
- $(repo)/specification/azurefleet/resource-manager/readme.powershell.md
64+
- $(repo)/specification/containerregistry/resource-manager/readme.powershell.md
65+
use-extension:
66+
dict:
67+
'@autorest/powershell': 3.x # always this value

src/aaz_dev/ps/tests/api_tests/__init__.py

Whitespace-only changes.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from ps.tests.common import CommandTestCase
2+
from utils.config import Config
3+
from utils.base64 import b64encode_str
4+
from utils.stage import AAZStageEnum
5+
# from cli.controller.az_module_manager import AzMainManager, AzExtensionManager
6+
import os
7+
import shutil
8+
import yaml
9+
10+
11+
class APIPowerShellTest(CommandTestCase):
12+
13+
def test_get_powershell_path(self):
14+
with self.app.test_client() as c:
15+
print(Config.POWERSHELL_PATH)
16+
rv = c.get("/PS/Powershell/Path")
17+
self.assertTrue(rv.status_code == 200)
18+
data = rv.get_json()
19+
self.assertTrue(data["path"] == Config.POWERSHELL_PATH)
20+
21+
def test_list_powershell_modules(self):
22+
config_dict = {}
23+
with self.app.test_client() as c:
24+
rv = c.get("/PS/Powershell/Modules")
25+
self.assertTrue(rv.status_code == 200)
26+
data = rv.get_json()
27+
self.assertTrue(len(data) > 100)
28+
self.assertTrue(all(module["name"].endswith(".Autorest") for module in data))
29+
for module in data:
30+
request_url = module["url"]
31+
rv = c.get(request_url)
32+
self.assertTrue(rv.status_code == 200)
33+
data = rv.get_json()
34+
if data["autorest_config"] is None:
35+
continue
36+
for key, value in data["autorest_config"].items():
37+
if key in ["directive", "commit", "input-file", "title", "module-version"]:
38+
continue
39+
if key not in config_dict:
40+
config_dict[key] = {
41+
"list": set(),
42+
"dict": {},
43+
"basic": set(),
44+
}
45+
if isinstance(value, list):
46+
config_dict[key]["list"].update(value)
47+
elif isinstance(value, dict):
48+
config_dict[key]["dict"].update(value)
49+
else:
50+
config_dict[key]["basic"].add(value)
51+
for key, value in config_dict.items():
52+
if not len(value["list"]):
53+
del value["list"]
54+
else:
55+
value["list"] = sorted(list(value["list"]))
56+
if not len(value["dict"]):
57+
del value["dict"]
58+
if not len(value["basic"]):
59+
del value["basic"]
60+
else:
61+
value["basic"] = sorted(list(value["basic"]))
62+
# with open("ps/templates/autorest/config_common_used_props.yaml", "w") as f:
63+
# yaml.dump(config_dict, f)

src/aaz_dev/ps/tests/common.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from app.tests.common import ApiTestCase
2+
from utils.config import Config
3+
# from command.tests.common import workspace_name
4+
# from swagger.utils.tools import swagger_resource_path_to_resource_id
5+
# from swagger.utils.source import SourceTypeEnum
6+
# from utils.plane import PlaneEnum
7+
# from utils.stage import AAZStageEnum
8+
# from utils.client import CloudEnum
9+
10+
11+
class CommandTestCase(ApiTestCase):
12+
13+
def __init__(self, *args, **kwargs):
14+
super().__init__(*args, **kwargs)

0 commit comments

Comments
 (0)