Skip to content

Commit 60e64e6

Browse files
author
Yalin Li
authored
Support CodeGen for extension creation (#266)
1 parent 87605f5 commit 60e64e6

7 files changed

Lines changed: 144 additions & 51 deletions

File tree

azdev/help.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,16 @@
168168

169169

170170
helps['extension create'] = """
171-
short-summary: Create a new Azure CLI extension template.
171+
short-summary: Create a new Azure CLI extension.
172172
examples:
173-
- name: Scaffold a new CLI extension named 'contoso'.
174-
text: azdev extension create contoso
175-
- name: Scaffold a new CLI extension with the azure-mgmt-contoso SDK.
173+
- name: Generate a new CLI extension named 'contoso' with local or remote azure-rest-api-specs repo.
174+
text: azdev extension create contoso --azure-rest-api-specs {azure-rest-api-specs repo path}
175+
- name: Generate a new CLI extension named 'contoso' with the default azure-rest-api-specs repo.
176176
text: >
177-
azdev extension create contoso --local-sdk {sdkPath} --operation-name ContosoOperations
178-
--client-name ContosoManagementClient --sdk-property contoso_name
177+
azdev extension create contoso
178+
- name: Generate a new CLI extension named 'contoso' with specified autorest.az release.
179+
text: >
180+
azdev extension create contoso --use=https://github.com/Azure/autorest.az/releases/download/1.4.0/autorest-az-1.4.0.tgz
179181
"""
180182

181183

azdev/operations/code_gen.py

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
import json
1010
import os
1111
import re
12+
import subprocess
1213

1314
from knack.log import get_logger
1415
from knack.prompting import prompt_y_n, prompt
1516
from knack.util import CLIError
1617

18+
import azdev.utilities.const as const
19+
1720
from azdev.utilities import (
18-
pip_cmd, display, heading, COMMAND_MODULE_PREFIX, EXTENSION_PREFIX, get_cli_repo_path, get_ext_repo_paths,
19-
find_files)
21+
pip_cmd, shell_cmd, display, heading, COMMAND_MODULE_PREFIX, EXTENSION_PREFIX, get_cli_repo_path,
22+
get_ext_repo_paths, find_files, require_virtual_env)
23+
24+
from urllib import request, error
25+
2026

2127
logger = get_logger(__name__)
2228

@@ -55,24 +61,26 @@ def create_module(mod_name='test', display_name=None, display_name_plural=None,
5561
_display_success_message(COMMAND_MODULE_PREFIX + mod_name, mod_name)
5662

5763

58-
def create_extension(ext_name='test', repo_name='azure-cli-extensions',
59-
display_name=None, display_name_plural=None,
60-
required_sdk=None, client_name=None, operation_name=None, sdk_property=None,
61-
not_preview=False, github_alias=None, local_sdk=None):
62-
repo_path = None
64+
def create_extension(ext_name, azure_rest_api_specs=const.GITHUB_SWAGGER_REPO_URL, branch=None, use=None):
65+
if not azure_rest_api_specs.startswith('http') and branch:
66+
raise CLIError('Cannot specify azure-rest-api-specs repo branch when using local one.')
67+
if not branch:
68+
branch = json.load(
69+
request.urlopen('https://api.github.com/repos/Azure/azure-rest-api-specs')).get('default_branch')
70+
require_virtual_env()
6371
repo_paths = get_ext_repo_paths()
64-
repo_path = next((x for x in repo_paths if x.endswith(repo_name)), None)
65-
72+
repo_path = next(
73+
(x for x in repo_paths if x.endswith(const.EXT_REPO_NAME) or x.endswith(const.EXT_REPO_NAME + '\\')), None)
6674
if not repo_path:
6775
raise CLIError('Unable to find `{}` repo. Have you cloned it and added '
68-
'with `azdev extension repo add`?'.format(repo_name))
69-
70-
_create_package(EXTENSION_PREFIX, os.path.join(repo_path, 'src'), True, ext_name, display_name,
71-
display_name_plural, required_sdk, client_name, operation_name, sdk_property, not_preview,
72-
local_sdk)
73-
_add_to_codeowners(repo_path, EXTENSION_PREFIX, ext_name, github_alias)
76+
'with `azdev extension repo add`?'.format(const.EXT_REPO_NAME))
77+
if not os.path.isdir(repo_path):
78+
raise CLIError("Invalid path {} in .azure config.".format(repo_path))
79+
swagger_readme_file_path = _get_swagger_readme_file_path(ext_name, azure_rest_api_specs, branch)
80+
_generate_extension(ext_name, repo_path, swagger_readme_file_path, use)
81+
_add_extension(ext_name, repo_path)
7482

75-
_display_success_message(EXTENSION_PREFIX + ext_name, ext_name)
83+
_display_success_message(ext_name, ext_name)
7684

7785

7886
def _display_success_message(package_name, group_name):
@@ -300,3 +308,75 @@ def _create_package(prefix, repo_path, is_ext, name='test', display_name=None, d
300308
result = pip_cmd('install -e {}'.format(new_package_path), "Installing `{}{}`...".format(prefix, name))
301309
if result.error:
302310
raise result.error # pylint: disable=raising-bad-type
311+
312+
313+
def _get_swagger_readme_file_path(ext_name, swagger_repo, branch):
314+
swagger_readme_file_path = None
315+
if swagger_repo == const.GITHUB_SWAGGER_REPO_URL or \
316+
(swagger_repo.startswith('https://') and swagger_repo.endswith('azure-rest-api-specs')):
317+
swagger_readme_file_path = '{}/blob/{}/specification/{}/resource-manager'.format(
318+
swagger_repo, branch, ext_name)
319+
# validate URL
320+
try:
321+
request.urlopen(swagger_readme_file_path)
322+
except error.HTTPError as ex:
323+
raise CLIError(
324+
'HTTPError: {}\nNo swagger readme file found in this URL: {}'.format(ex.code, swagger_readme_file_path))
325+
else:
326+
swagger_readme_file_path = os.path.join(swagger_repo, 'specification', ext_name, 'resource-manager')
327+
if not os.path.isdir(swagger_readme_file_path):
328+
raise CLIError("The path {} does not exist.".format(swagger_readme_file_path))
329+
return swagger_readme_file_path
330+
331+
332+
# pylint: disable=too-many-statements
333+
def _generate_extension(ext_name, repo_path, swagger_readme_file_path, use):
334+
heading('Start generating extension {}.'.format(ext_name))
335+
# check if npm is installed
336+
try:
337+
shell_cmd('npm --version', stdout=subprocess.DEVNULL, raise_ex=False)
338+
except CLIError as ex:
339+
raise CLIError('{}\nPlease install npm.'.format(ex))
340+
display('Installing autorest...\n')
341+
if const.IS_WINDOWS:
342+
try:
343+
shell_cmd('npm install -g autorest', raise_ex=False)
344+
except CLIError as ex:
345+
raise CLIError("Failed to install autorest.\n{}".format(ex))
346+
else:
347+
try:
348+
shell_cmd('npm install -g autorest', stderr=subprocess.DEVNULL, raise_ex=False)
349+
except CLIError as ex:
350+
path = os.environ['PATH']
351+
# check if npm is installed through nvm
352+
if os.environ.get('NVM_DIR'):
353+
raise ex
354+
# check if user using specific node version and manually add it to the os env PATH
355+
node_version = shell_cmd('node --version', capture_output=True).result
356+
if 'node/' + node_version + '/bin' in path:
357+
raise ex
358+
# create a new directory for npm global installations, to avoid using sudo in installing autorest
359+
npm_path = os.path.join(os.environ['HOME'], '.npm-packages')
360+
if not os.path.isdir(npm_path):
361+
os.mkdir(npm_path)
362+
npm_prefix = shell_cmd('npm prefix -g', capture_output=True).result
363+
shell_cmd('npm config set prefix ' + npm_path)
364+
os.environ['PATH'] = path + ':' + os.path.join(npm_path, 'bin')
365+
os.environ['MANPATH'] = os.path.join(npm_path, 'share', 'man')
366+
shell_cmd('npm install -g autorest')
367+
shell_cmd('npm config set prefix ' + npm_prefix)
368+
# update autorest core
369+
shell_cmd('autorest --latest')
370+
if not use:
371+
cmd = 'autorest --az --azure-cli-extension-folder={} {}'.format(repo_path, swagger_readme_file_path)
372+
else:
373+
cmd = 'autorest --az --azure-cli-extension-folder={} {} --use={}'.format(
374+
repo_path, swagger_readme_file_path, use)
375+
shell_cmd(cmd, message=True)
376+
377+
378+
def _add_extension(ext_name, repo_path):
379+
new_package_path = os.path.join(repo_path, 'src', ext_name)
380+
result = pip_cmd('install -e {}'.format(new_package_path), "Adding extension `{}`...".format(new_package_path))
381+
if result.error:
382+
raise result.error

azdev/operations/extensions/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616

1717
from azdev.utilities import (
1818
cmd, py_cmd, pip_cmd, display, get_ext_repo_paths, find_files, get_azure_config, get_azdev_config,
19-
require_azure_cli, heading, subheading, EXTENSION_PREFIX)
19+
require_azure_cli, require_virtual_env, heading, subheading, EXTENSION_PREFIX)
2020

2121
logger = get_logger(__name__)
2222

2323

2424
def add_extension(extensions):
25-
25+
require_virtual_env()
2626
ext_paths = get_ext_repo_paths()
2727
all_extensions = find_files(ext_paths, 'setup.py')
2828

@@ -47,7 +47,7 @@ def add_extension(extensions):
4747

4848

4949
def remove_extension(extensions):
50-
50+
require_virtual_env()
5151
ext_paths = get_ext_repo_paths()
5252
installed_paths = find_files(ext_paths, '*.*-info')
5353
paths_to_remove = []
@@ -103,7 +103,7 @@ def _collect(path, depth=0, max_depth=3):
103103

104104
def list_extensions():
105105
from glob import glob
106-
106+
require_virtual_env()
107107
azure_config = get_azure_config()
108108
dev_sources = azure_config.get('extension', 'dev_sources', None)
109109
dev_sources = dev_sources.split(',') if dev_sources else []
@@ -143,8 +143,8 @@ def _get_sha256sum(a_file):
143143

144144

145145
def add_extension_repo(repos):
146-
147146
from azdev.operations.setup import _check_repo
147+
require_virtual_env()
148148
az_config = get_azure_config()
149149
env_config = get_azdev_config()
150150
dev_sources = az_config.get('extension', 'dev_sources', None)
@@ -161,7 +161,7 @@ def add_extension_repo(repos):
161161

162162

163163
def remove_extension_repo(repos):
164-
164+
require_virtual_env()
165165
az_config = get_azure_config()
166166
env_config = get_azdev_config()
167167
dev_sources = az_config.get('extension', 'dev_sources', None)
@@ -177,7 +177,7 @@ def remove_extension_repo(repos):
177177

178178

179179
def list_extension_repos():
180-
180+
require_virtual_env()
181181
az_config = get_azure_config()
182182
dev_sources = az_config.get('extension', 'dev_sources', None)
183183
return dev_sources.split(',') if dev_sources else dev_sources
@@ -188,7 +188,7 @@ def update_extension_index(extensions):
188188
import tempfile
189189

190190
from .util import get_ext_metadata, get_whl_from_url
191-
191+
require_virtual_env()
192192
ext_repos = get_ext_repo_paths()
193193
index_path = next((x for x in find_files(ext_repos, 'index.json') if 'azure-cli-extensions' in x), None)
194194
if not index_path:
@@ -245,6 +245,7 @@ def update_extension_index(extensions):
245245

246246

247247
def build_extensions(extensions, dist_dir='dist'):
248+
require_virtual_env()
248249
ext_paths = get_ext_repo_paths()
249250
all_extensions = find_files(ext_paths, 'setup.py')
250251

@@ -274,8 +275,8 @@ def publish_extensions(extensions, storage_account, storage_account_key, storage
274275
dist_dir='dist', update_index=False, yes=False):
275276
from azure.storage.blob import BlockBlobService
276277

278+
require_virtual_env()
277279
heading('Publish Extensions')
278-
279280
require_azure_cli()
280281

281282
# rebuild the extensions

azdev/operations/help/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,16 @@
1313
import tempfile
1414

1515
from subprocess import check_call, check_output, CalledProcessError
16-
17-
from knack.util import CLIError
18-
from knack.log import get_logger
19-
20-
from azure.cli.core.extension.operations import list_available_extensions, list_extensions as list_cli_extensions # pylint: disable=import-error
2116
from azdev.utilities import (
2217
display, heading, subheading,
2318
get_cli_repo_path, get_path_table,
2419
require_virtual_env
2520
)
26-
2721
from azdev.utilities.tools import require_azure_cli
2822
from azdev.operations.extensions import list_extensions as list_dev_cli_extensions
23+
from knack.util import CLIError
24+
from knack.log import get_logger
25+
require_virtual_env()
2926

3027
DOC_MAP_NAME = 'doc_source_map.json'
3128
HELP_FILE_NAME = '_help.py'
@@ -106,6 +103,7 @@ def generate_cli_ref_docs(output_dir=None, output_type=None, all_profiles=None):
106103

107104

108105
def generate_extension_ref_docs(output_dir=None, output_type=None):
106+
require_virtual_env()
109107
# require that azure cli installed
110108
require_azure_cli()
111109
output_dir = _process_ref_doc_output_dir(output_dir)
@@ -261,6 +259,7 @@ def _get_profiles():
261259

262260

263261
def _warn_if_exts_installed():
262+
from azure.cli.core.extension.operations import list_extensions as list_cli_extensions # pylint: disable=import-error
264263
cli_extensions, dev_cli_extensions = list_cli_extensions(), list_dev_cli_extensions()
265264
if cli_extensions:
266265
_logger.warning("One or more CLI Extensions are installed and will be included in ref doc output.")
@@ -277,6 +276,7 @@ def _get_available_extension_urls():
277276
278277
:return: list of 3-tuples in the form of '(extension_name, extension_file_name, extensions_download_url)'
279278
"""
279+
from azure.cli.core.extension.operations import list_available_extensions # pylint: disable=import-error
280280
all_pub_extensions = list_available_extensions(show_details=True)
281281
compatible_extensions = list_available_extensions()
282282

azdev/operations/legal.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ def check_license_headers():
4444

4545
cli_path = get_cli_repo_path()
4646
all_paths = [cli_path]
47-
for path in get_ext_repo_paths():
48-
all_paths.append(path)
47+
try:
48+
ext_repo = get_ext_repo_paths()
49+
for path in ext_repo:
50+
all_paths.append(path)
51+
except CLIError:
52+
display("No CLI ext path, running check only on modules")
4953

5054
files_without_header = []
5155
for path in all_paths:

azdev/params.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,22 @@ def load_arguments(self, _):
141141
with ArgumentsContext(self, 'extension create') as c:
142142
c.positional('ext_name', help='Name of the extension to create.')
143143

144-
for scope in ['extension create', 'cli create']:
145-
with ArgumentsContext(self, scope) as c:
146-
c.argument('github_alias', help='Github alias for the individual who will be the code owner for this package.')
147-
c.argument('not_preview', action='store_true', help='Do not create template commands under a "Preview" status.')
148-
c.argument('required_sdk', help='Name and version of the underlying Azure SDK that is published on PyPI. (ex: azure-mgmt-contoso==0.1.0).', arg_group='SDK')
149-
c.argument('local_sdk', help='Path to a locally saved SDK. Use if your SDK is not available on PyPI.', arg_group='SDK')
150-
c.argument('client_name', help='Name of the Python SDK client object (ex: ContosoManagementClient).', arg_group='SDK')
151-
c.argument('operation_name', help='Name of the principal Python SDK operation class (ex: ContosoOperations).', arg_group='SDK')
152-
c.argument('sdk_property', help='The name of the Python variable that describes the main object name in the SDK calls (i.e.: account_name)', arg_group='SDK')
153-
c.argument('repo_name', help='Name of the repo the extension will exist in.')
154-
c.argument('display_name', arg_group='Help', help='Description to display in help text.')
155-
c.argument('display_name_plural', arg_group='Help', help='Description to display in help text when plural.')
144+
with ArgumentsContext(self, 'extension create') as c:
145+
c.argument('azure_rest_api_specs', help='The local path or GitHub to azure-rest-api-specs repo.')
146+
c.argument('branch', help='The repo branch when using remote azure-rest-api-specs repo. Default: the default branch set in this repo.')
147+
c.argument('use', help='The URL for downloading autorest.az tgz file. You can get all releases here: https://github.com/Azure/autorest.az/releases')
148+
149+
with ArgumentsContext(self, 'cli create') as c:
150+
c.argument('github_alias', help='Github alias for the individual who will be the code owner for this package.')
151+
c.argument('not_preview', action='store_true', help='Do not create template commands under a "Preview" status.')
152+
c.argument('required_sdk', help='Name and version of the underlying Azure SDK that is published on PyPI. (ex: azure-mgmt-contoso==0.1.0).', arg_group='SDK')
153+
c.argument('local_sdk', help='Path to a locally saved SDK. Use if your SDK is not available on PyPI.', arg_group='SDK')
154+
c.argument('client_name', help='Name of the Python SDK client object (ex: ContosoManagementClient).', arg_group='SDK')
155+
c.argument('operation_name', help='Name of the principal Python SDK operation class (ex: ContosoOperations).', arg_group='SDK')
156+
c.argument('sdk_property', help='The name of the Python variable that describes the main object name in the SDK calls (i.e.: account_name)', arg_group='SDK')
157+
c.argument('repo_name', help='Name of the repo the extension will exist in.')
158+
c.argument('display_name', arg_group='Help', help='Description to display in help text.')
159+
c.argument('display_name_plural', arg_group='Help', help='Description to display in help text when plural.')
156160

157161
with ArgumentsContext(self, 'cli generate-docs') as c:
158162
c.argument('all_profiles', action='store_true',

azdev/utilities/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
COMMAND_MODULE_PREFIX = 'azure-cli-'
1313
CONFIG_NAME = 'config'
1414
EXTENSION_PREFIX = 'azext_'
15+
EXT_REPO_NAME = 'azure-cli-extensions'
1516
EXT_SECTION = 'extension'
17+
GITHUB_SWAGGER_REPO_URL = 'https://github.com/Azure/azure-rest-api-specs'
1618
IS_WINDOWS = sys.platform.lower() in ['windows', 'win32']
1719
PIP_E_CMD = 'pip install -e '
1820
PIP_R_CMD = 'pip install -r '

0 commit comments

Comments
 (0)