Skip to content

Commit 2577c84

Browse files
committed
implement to load the powershell modules
1 parent bdb091b commit 2577c84

File tree

12 files changed

+389
-173
lines changed

12 files changed

+389
-173
lines changed

src/aaz_dev/ps/api/powershell.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def powershell_modules():
5454
def powershell_module(module_names):
5555
manager = PSModuleManager()
5656
if request.method == "GET":
57-
result = manager.load_module(module_names)
58-
# result = module.to_primitive()
57+
module = manager.load_module(module_names)
58+
result = module.to_primitive()
5959
result['url'] = url_for('powershell.powershell_module', module_names=result['name'])
6060
elif request.method == "PUT":
6161
raise NotImplementedError()

src/aaz_dev/ps/controller/autorest_configuration_generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ def generate_config(self):
106106
if not swagger_resources:
107107
raise ResourceNotFind("Resources not find in Swagger")
108108

109-
readme_parts= rp._readme_path.split(os.sep)
109+
# TODO: use the correct readme file
110+
readme_parts= rp._readme_paths[0].split(os.sep)
110111
ps_cfg.readme_file = '/'.join(readme_parts[readme_parts.index("specification"):])
111112
ps_cfg.version = "0.1.0"
112113
ps_cfg.module_name = mod_names.split("/")[0]
Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import logging
22
import os
3-
import yaml
43

54
from utils.config import Config
6-
5+
from utils.plane import PlaneEnum
6+
from utils.readme_helper import parse_readme_file
7+
from ps.model import PSModuleConfig
8+
from swagger.controller.specs_manager import SwaggerSpecsManager
9+
from swagger.model.specs import SwaggerModule
10+
from command.controller.specs_manager import AAZSpecsManager
11+
from swagger.model.specs import OpenAPIResourceProvider
12+
from swagger.utils.tools import resolve_path_to_uri
713
logger = logging.getLogger('backend')
814

915

@@ -12,6 +18,20 @@ class PSModuleManager:
1218
def __init__(self):
1319
module_folder = self._find_module_folder()
1420
self.folder = module_folder
21+
self._aaz_specs = None
22+
self._swagger_specs = None
23+
24+
@property
25+
def aaz_specs(self):
26+
if not self._aaz_specs:
27+
self._aaz_specs = AAZSpecsManager()
28+
return self._aaz_specs
29+
30+
@property
31+
def swagger_specs(self):
32+
if not self._swagger_specs:
33+
self._swagger_specs = SwaggerSpecsManager()
34+
return self._swagger_specs
1535

1636
def _find_module_folder(self):
1737
powershell_folder = Config.POWERSHELL_PATH
@@ -48,12 +68,8 @@ def load_module(self, module_names):
4868
folder = os.path.join(self.folder, *module_names)
4969
if not os.path.exists(folder):
5070
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-
}
71+
config = self.load_module_config(module_names)
72+
return config
5773

5874
def load_autorest_config(self, module_names):
5975
if isinstance(module_names, str):
@@ -62,33 +78,96 @@ def load_autorest_config(self, module_names):
6278
readme_file = os.path.join(folder, "README.md")
6379
if not os.path.exists(readme_file):
6480
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)
81+
content = parse_readme_file(readme_file)
82+
return content['config'], content['title']
83+
84+
def load_module_config(self, module_names):
8785
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-
}
86+
autorest_config, readme_title = self.load_autorest_config(module_names)
87+
except:
88+
logger.error(f"Failed to load autorest config for module: {module_names}, error: {e}")
89+
raise
90+
91+
config = PSModuleConfig()
92+
config.name = "/".join(module_names)
93+
config.folder = self.folder
94+
if not autorest_config:
95+
raise ValueError(f"autorest config not found in README.md for module: {config.name}")
96+
97+
# config.swagger = autorest_config
98+
repo = autorest_config.get('repo', "https://github.com/Azure/azure-rest-api-specs/blob/$(commit)")
99+
if commit := autorest_config.get('commit'):
100+
repo = repo.replace("$(commit)", commit)
101+
if "$(commit)" in repo:
102+
# make sure the repo is valid https link or valid folder path
103+
raise ValueError(f"commit is not defined in autorest config for module: {config.name}")
104+
config.repo = repo
105+
106+
readme_file = None
107+
for required_file in autorest_config['require']:
108+
if required_file.startswith('$(repo)/') and required_file.endswith('/readme.md'):
109+
readme_file = required_file.replace('$(repo)/', '')
110+
break
111+
112+
if not readme_file:
113+
# search the readme.md in the swagger specs folder
114+
for input_file in autorest_config.get('input-file', []):
115+
if "/specification/" in input_file:
116+
folder_names = input_file.split("/specification/")[1].split("/")[:-1]
117+
path = os.path.join(self.swagger_specs.specs.spec_folder_path, *folder_names)
118+
while path != self.swagger_specs.specs.spec_folder_path:
119+
if os.path.exists(os.path.join(path, "readme.md")):
120+
readme_file = os.path.join(path, "readme.md")
121+
break
122+
path = os.path.dirname(path)
123+
if readme_file:
124+
readme_file = resolve_path_to_uri(readme_file)
125+
break
126+
if not readme_file:
127+
raise ValueError(f"swagger readme.md not defined in autorest config for module: {config.name}")
128+
129+
# use the local swagger specs to find the resource provider even the repo is in remote
130+
# we can always suppose the local swagger specs will always be newer than the used commit in submitted azure.powershell code
131+
rp = None
132+
readme_config = None
133+
plane = PlaneEnum.Mgmt if "resource-manager" in readme_file else PlaneEnum._Data
134+
for module in self.swagger_specs.get_modules(plane):
135+
module_relative_path = resolve_path_to_uri(module.folder_path) + "/"
136+
if readme_file.startswith(module_relative_path):
137+
for resource_provider in module.get_resource_providers():
138+
if not isinstance(resource_provider, OpenAPIResourceProvider):
139+
continue
140+
readme_config = resource_provider.load_readme_config(readme_file)
141+
if readme_config:
142+
rp = resource_provider
143+
break
144+
if rp:
145+
break
146+
if not rp:
147+
raise ValueError(f"Resource provider not found in autorest config for module: {config.name}")
148+
config.rp = rp
149+
config.swagger = str(rp)
150+
151+
if tag := autorest_config.get('tag'):
152+
config.tag = tag
153+
if input_files := autorest_config.get('input-file'):
154+
config.input_files = []
155+
for input_file in input_files:
156+
if input_file.startswith('$(repo)/'):
157+
input_file = input_file.replace('$(repo)/', '')
158+
config.input_files.append(input_file)
159+
if not config.input_files and not config.tag:
160+
config.tag = readme_config.get('tag', None)
161+
162+
if readme_title.startswith("Az."):
163+
config.service_name = readme_title.split(".")[1]
164+
if title := autorest_config.get('title'):
165+
config.title = title
166+
else:
167+
# get title from swagger readme
168+
config.title = readme_config.get('title', None)
169+
170+
if not config.title:
171+
raise ValueError(f"Title not found in autorest config or swagger readme for module: {config.name}")
172+
173+
return config

src/aaz_dev/ps/model/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._module_config import PSModuleConfig
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
3+
from schematics.models import Model
4+
from schematics.types import ModelType, DictType, StringType, ListType
5+
6+
7+
class PSModuleConfig(Model):
8+
name = StringType(required=True)
9+
folder = StringType(required=True)
10+
repo = StringType(required=True) # swagger repo path, https://github.com/Azure/<repo_name>/tree/<commit> or $(this-folder)/../../../<repo_name>
11+
swagger = StringType(required=True) # swagger resource provider, <plane>/<path:mod_names>/ResourceProviders/<rp_name>
12+
13+
# use tag or input files to select the swagger apis
14+
tag = StringType() # if the tag selected, the input_files will be ignored
15+
input_files = ListType(
16+
StringType(),
17+
serialized_name='inputFiles',
18+
deserialize_from='inputFiles',
19+
) # The input file should not contain $(repo) and can be directly appended to the repo
20+
21+
title = StringType(required=True) # the required value for the autorest configuration
22+
service_name = StringType(
23+
required=True,
24+
serialized_name='serviceName',
25+
deserialize_from='serviceName',
26+
) # by default calculated from the title with this implementation https://github.com/Azure/autorest.powershell/blob/main/powershell/plugins/plugin-tweak-model.ts#L25-L33
27+
28+
# those default value defined in the noprofile.md configuration https://github.com/Azure/azure-powershell/blob/generation/src/readme.azure.noprofile.md
29+
module_name = StringType(
30+
required=True,
31+
serialized_name='moduleName',
32+
deserialize_from='moduleName',
33+
default='$(prefix).$(service-name)'
34+
) # by default $(prefix).$(service-name)
35+
namespace = StringType(
36+
required=True,
37+
default='Microsoft.Azure.PowerShell.Cmdlets.$(service-name)'
38+
) # used for sub module to define the powershell class namespace, by default Microsoft.Azure.PowerShell.Cmdlets.$(service-name)
39+
subject_prefix = StringType(
40+
required=True,
41+
serialized_name='subjectPrefix',
42+
deserialize_from='subjectPrefix',
43+
default='$(service-name)'
44+
) # the default value $(service-name)
45+
# root_module_name = StringType() # used for sub module to generate the code in root module if there are multiple sub modules
46+
47+
prefix = StringType(
48+
default='Az',
49+
) # Not allowed to change
50+
51+
class Options:
52+
serialize_when_none = False
53+
54+
# swagger related properties
55+
56+
def __init__(self, *args, **kwargs):
57+
super().__init__(*args, **kwargs)
58+
self.rp = None
59+
60+
@property
61+
def repo_name(self):
62+
return self.repo.split('/tree/', 1)[0].split('/')[-1]
63+
64+
@property
65+
def commit(self):
66+
parts = self.repo.split('/tree/', 1)
67+
if len(parts) == 2:
68+
return parts[1].split('/')[0]
69+
return None
70+
71+
@property
72+
def plane(self):
73+
return self.swagger.split('/')[0]
74+
75+
@property
76+
def mod_names(self):
77+
return self.swagger.split("/ResourceProviders/")[0].split('/')[1:]
78+
79+
@property
80+
def rp_name(self):
81+
return self.swagger.split("/ResourceProviders/")[1].split('/')[0]
Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
from ps.tests.common import CommandTestCase
22
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
93

104

115
class APIPowerShellTest(CommandTestCase):
@@ -19,45 +13,20 @@ def test_get_powershell_path(self):
1913
self.assertTrue(data["path"] == Config.POWERSHELL_PATH)
2014

2115
def test_list_powershell_modules(self):
22-
config_dict = {}
2316
with self.app.test_client() as c:
2417
rv = c.get("/PS/Powershell/Modules")
2518
self.assertTrue(rv.status_code == 200)
2619
data = rv.get_json()
2720
self.assertTrue(len(data) > 100)
2821
self.assertTrue(all(module["name"].endswith(".Autorest") for module in data))
2922
for module in data:
23+
if module["name"] in [
24+
"Communication/EmailServicedata.Autorest",
25+
"ManagedServiceIdentity/ManagedServiceIdentity.Autorest", "VoiceServices/VoiceServices.Autorest",
26+
"Resources/MSGraph.Autorest", "Migrate/Migrate.Autorest"
27+
]:
28+
continue
3029
request_url = module["url"]
3130
rv = c.get(request_url)
3231
self.assertTrue(rv.status_code == 200)
3332
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/swagger/model/specs/_resource_provider.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,29 +80,6 @@ def load_readme_config(self, readme_file):
8080
return parse_readme_file(readme_path)['config']
8181
return None
8282

83-
@property
84-
def default_tag(self):
85-
if self._readme_path is None:
86-
return None
87-
88-
with open(self._readme_path, 'r', encoding='utf-8') as f:
89-
readme = f.read()
90-
lines = readme.split('\n')
91-
for i in range(len(lines)):
92-
line = lines[i]
93-
if line.startswith('### Basic Information'):
94-
lines = lines[i+1:]
95-
break
96-
latest_tag = None
97-
for i in range(len(lines)):
98-
line = lines[i]
99-
if line.startswith('##'):
100-
break
101-
if line.startswith('tag:'):
102-
latest_tag = line.split(':')[-1].strip()
103-
break
104-
return latest_tag
105-
10683
@property
10784
def tags(self):
10885
if self._tags is None:

0 commit comments

Comments
 (0)