diff --git a/src/containerapp/.gitignore b/src/containerapp/.gitignore new file mode 100644 index 00000000000..d79675e9460 --- /dev/null +++ b/src/containerapp/.gitignore @@ -0,0 +1,3 @@ +# Temporary folders for shared libraries +azext_containerapp/bin/ +azext_containerapp/bin/* \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index fb750498314..091b8d6af1a 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -297,15 +297,20 @@ # ContainerApp Patch ImageProperties = { "imageName": None, - "targetContainerAppName": None + "targetContainerName": None, + "targetContainerAppName": None, + "revisionMode": None, } ImagePatchableCheck = { "targetContainerAppName": None, + "targetContainerName": None, + "revisionMode": None, + "targetImageName": None, "oldRunImage": None, "newRunImage": None, "id": None, - "reason": None + "reason": None, } OryxMarinerRunImgTagProperty = { diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index a64f3e31bc5..d3fd1743c92 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -321,7 +321,7 @@ def load_arguments(self, _): c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') - c.argument('source', help='Local directory path containing the application source and Dockerfile for building the container image. Preview: If no Dockerfile is present, a container image is generated using Oryx. See the supported Oryx runtimes here: https://github.com/microsoft/Oryx/blob/main/doc/supportedRuntimeVersions.md.') + c.argument('source', help='Local directory path containing the application source and Dockerfile for building the container image. Preview: If no Dockerfile is present, a container image is generated using buildpacks. If Docker is not running or buildpacks cannot be used, Oryx will be used to generate the image. See the supported Oryx runtimes here: https://github.com/microsoft/Oryx/blob/main/doc/supportedRuntimeVersions.md.') c.argument('image', options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") c.argument('browse', help='Open the app in a web browser after creation and deployment, if possible.') c.argument('workload_profile_name', options_list=['--workload-profile-name', '-w'], help='The friendly name for the workload profile') @@ -418,4 +418,9 @@ def load_arguments(self, _): with self.argument_context('containerapp patch list') as c: c.argument('resource_group_name', options_list=['--rg','-g'], configured_default='resource_group_name', id_part=None) c.argument('environment', options_list=['--environment'], help='Name or resource id of the Container App environment.') + c.argument('show_all', options_list=['--show-all'],help='Show all patchable and non-patchable containerapps') + + with self.argument_context('containerapp patch run') as c: + c.argument('resource_group_name', option_list=['--rg','-g'], configured_default='resource_group_name', id_part=None) + c.argument('environment', options_list=['--environment'], help='Name or resource id of the Container App environment.') c.argument('show_all', options_list=['--show-all'],help='Show all patchable and non-patchable containerapps') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 11034fbcdb0..bb2da6db397 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -8,6 +8,7 @@ from tempfile import NamedTemporaryFile from urllib.parse import urlparse import requests +import subprocess from azure.cli.core.azclierror import ( RequiredArgumentMissingError, @@ -48,7 +49,9 @@ register_provider_if_needed, validate_environment_location, list_environment_locations, - format_location + format_location, + is_docker_running, + get_pack_exec_path ) from ._constants import (MAXIMUM_SECRET_LENGTH, @@ -354,7 +357,60 @@ def create_acr(self): self.cmd.cli_ctx, registry_name ) - def build_container_from_source(self, image_name, source): + def build_container_from_source_with_buildpack(self, image_name, source): + # Ensure that Docker is running + if not is_docker_running(): + raise CLIError("Docker is not running. Please start Docker and try again.") + + # Ensure that the pack CLI is installed + pack_exec_path = get_pack_exec_path() + if pack_exec_path == "": + raise CLIError("The pack CLI could not be installed.") + + logger.info("Docker is running and pack CLI is installed; attempting to use buildpacks to build container image...") + + registry_name = self.registry_server.lower() + image_name = f"{registry_name}/{image_name}" + builder_image_name="mcr.microsoft.com/oryx/builder:builder-dotnet-7.0" + + # Ensure that the builder is trusted + command = [pack_exec_path, 'config', 'default-builder', builder_image_name] + logger.debug(f"Calling '{' '.join(command)}'") + try: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode != 0: + raise CLIError(f"Error thrown when running 'pack config': {stderr.decode('utf-8')}") + logger.debug(f"Successfully set the default builder to {builder_image_name}.") + except Exception as ex: + raise CLIError(f"Unable to run 'pack build' command to produce runnable application image: {ex}") + + # Run 'pack build' to produce a runnable application image for the Container App + command = [pack_exec_path, 'build', image_name, '--builder', builder_image_name, '--path', source] + logger.debug(f"Calling '{' '.join(command)}'") + try: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode != 0: + raise CLIError(f"Error thrown when running 'pack build': {stderr.decode('utf-8')}") + logger.debug(f"Successfully built image {image_name} using buildpacks.") + except Exception as ex: + raise CLIError(f"Unable to run 'pack build' command to produce runnable application image: {ex}") + + # Run 'docker push' to push the image to the ACR + command = ['docker', 'push', image_name] + logger.debug(f"Calling '{' '.join(command)}'") + logger.warning(f"Built image {image_name} locally using buildpacks, attempting to push to registry...") + try: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode != 0: + raise CLIError(f"Error thrown when running 'docker push': {stderr.decode('utf-8')}") + logger.debug(f"Successfully pushed image {image_name} to ACR.") + except Exception as ex: + raise CLIError(f"Unable to run 'docker push' command to push image to ACR: {ex}") + + def build_container_from_source_with_acr_task(self, image_name, source): from azure.cli.command_modules.acr.task import acr_task_create, acr_task_run from azure.cli.command_modules.acr._client_factory import cf_acr_tasks, cf_acr_runs from azure.cli.core.profiles import ResourceType @@ -414,7 +470,18 @@ def run_acr_build(self, dockerfile, source, quiet=False, build_from_source=False if build_from_source: # TODO should we prompt for confirmation here? logger.warning("No dockerfile detected. Attempting to build a container directly from the provided source...") - self.build_container_from_source(image_name, source) + + try: + # First try to build source using buildpacks + logger.warning("Attempting to build image using buildpacks...") + self.build_container_from_source_with_buildpack(image_name, source) + return + except CLIError as e: + logger.warning(f"Unable to use buildpacks to build source: {e}\n Falling back to ACR Task...") + + # If we're unable to use the buildpack, build source using an ACR Task + logger.warning("Attempting to build image using ACR Task...") + self.build_container_from_source_with_acr_task(image_name, source) else: queue_acr_build( self.cmd, diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 833ad19e7e9..6505145ebc5 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -7,8 +7,10 @@ import time import json import platform -import hashlib +import docker +import os import requests +import hashlib import packaging.version as SemVer import re @@ -1720,14 +1722,78 @@ def format_location(location=None): return location.lower().replace(" ", "").replace("(", "").replace(")", "") return location +def is_docker_running(): + # check to see if docker is running + client = None + out = True + try: + client = docker.from_env() + # need any command that will show the docker daemon is not running + client.containers.list() + except docker.errors.DockerException as e: + logger.warning(f"Exception thrown when getting Docker client: {e}") + out = False + finally: + if client: + client.close() + return out + + +def get_pack_exec_path(): + try: + dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "azext_containerapp") + bin_folder = dir_path + "/bin" + if not os.path.exists(bin_folder): + os.makedirs(bin_folder) + + exec_name = "" + host_os = platform.system() + if host_os == "Windows": + exec_name = "pack-v0.29.0-windows.exe" + elif host_os == "Linux": + exec_name = "pack-v0.29.0-linux" + elif host_os == "Darwin": + exec_name = "pack-v0.29.0-macos" + else: + raise Exception(f"Unsupported host OS: {host_os}") + + exec_path = os.path.join(bin_folder, exec_name) + if os.path.exists(exec_path): + return exec_path + + # Attempt to install the pack CLI + url = f"https://cormteststorage.blob.core.windows.net/pack/{exec_name}" + r = requests.get(url) + with open(exec_path, "wb") as f: + f.write(r.content) + print(f"Successfully installed pack CLI to {exec_path}\n") + return exec_path + + except Exception as e: + # Swallow any exceptions thrown when attempting to install pack CLI + print(f"Failed to install pack CLI: {e}\n") + + return "" + def patchableCheck(repoTagSplit: str, oryxBuilderRunImgTags, bom): tagProp = parseOryxMarinerTag(repoTagSplit) if tagProp is None: result = ImagePatchableCheck result["targetContainerAppName"] = bom["targetContainerAppName"] + result["revisionMode"] = bom["revisionMode"] + result["targetContainerName"] = bom["targetContainerName"] + result["targetImageName"] = bom["image_name"] result["oldRunImage"] = repoTagSplit result["reason"] = "Image not based on dotnet Mariner." return result + # elif len(str(tagProp["version"]).split(".")) == 2: + # result = ImagePatchableCheck + # result["targetContainerAppName"] = bom["targetContainerAppName"] + # result["revisionMode"] = bom["revisionMode"] + # result["targetContainerName"] = bom["targetContainerName"] + # result["oldRunImage"] = repoTagSplit + # result["reason"] = "Image is a patchless version." + # return result repoTagSplit = repoTagSplit.split("-") if repoTagSplit[1] == "dotnet": matchingVersionInfo = oryxBuilderRunImgTags[repoTagSplit[2]][str(tagProp["version"].major) + "." + str(tagProp["version"].minor)][tagProp["support"]][tagProp["marinerVersion"]] @@ -1736,11 +1802,14 @@ def patchableCheck(repoTagSplit: str, oryxBuilderRunImgTags, bom): if tagProp["version"] < matchingVersionInfo[0]["version"]: result = ImagePatchableCheck result["targetContainerAppName"] = bom["targetContainerAppName"] + result["revisionMode"] = bom["revisionMode"] + result["targetImageName"] = bom["image_name"] + result["targetContainerName"] = bom["targetContainerName"] result["oldRunImage"] = tagProp["fullTag"] if (tagProp["version"].minor == matchingVersionInfo[0]["version"].minor) and (tagProp["version"].micro < matchingVersionInfo[0]["version"].micro): # Patchable result["newRunImage"] = "mcr.microsoft.com/oryx/builder:" + matchingVersionInfo[0]["fullTag"] - result["id"] = hashlib.md5(str(result["oldRunImage"] + result["targetContainerAppName"] + result["newRunImage"]).encode()).hexdigest() + result["id"] = hashlib.md5(str(result["oldRunImage"] + result["targetContainerName"] + result["targetContainerAppName"] + result["newRunImage"]).encode()).hexdigest() result["reason"] = "New security patch released for your current run image." else: # Not patchable @@ -1749,6 +1818,9 @@ def patchableCheck(repoTagSplit: str, oryxBuilderRunImgTags, bom): else: result = ImagePatchableCheck result["targetContainerAppName"] = bom["targetContainerAppName"] + result["revisionMode"] = bom["revisionMode"] + result["targetContainerName"] = bom["targetContainerName"] + result["targetImageName"] = bom["image_name"] result["oldRunImage"] = tagProp["fullTag"] result["reason"] = "You're already up to date!" return result diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7a311631b47..4ed96005fd1 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -202,3 +202,4 @@ def load_command_table(self, _): with self.command_group('containerapp patch', is_preview=True) as g: g.custom_command('list', 'patch_list') + g.custom_command('run', 'patch_run') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 11e985cd19e..f53589d8157 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4301,24 +4301,14 @@ def delete_workload_profile(cmd, resource_group_name, env_name, workload_profile except Exception as e: handle_raw_exception(e) -def patch_list(cmd, resource_group_name, managed_env=None, show_all=False): - if(managed_env): - caList = list_containerapp(cmd, resource_group_name, managed_env) - else: - envList = list_managed_environments(cmd, resource_group_name) - envNames = [] - for env in envList: - envNames.append(env["name"]) - caList = [] - for envName in envNames: - caList += list_containerapp(cmd, resource_group_name, envName) - print(caList) +def patch_list(cmd, resource_group_name, managed_env, show_all=False): + caList = list_containerapp(cmd, resource_group_name, managed_env) imgs = [] if caList: for ca in caList: containers = ca["properties"]["template"]["containers"] for container in containers: - result = dict(imageName=container["image"], targetContainerAppName=container["name"]) + result = dict(imageName=container["image"], targetContainerName=container["name"], targetContainerAppName=ca["name"], revisionMode=ca["properties"]["configuration"]["activeRevisionsMode"]) imgs.append(result) # Get the BOM of the images @@ -4328,25 +4318,28 @@ def patch_list(cmd, resource_group_name, managed_env=None, show_all=False): ## For production # for img in imgs: - subprocess.run("pack inspect-image " + img["imageName"] + " --output json > ./bom.json 2>&1", shell=True) - with open("./bom.json", "rb") as f: - bom = None - lines = f.read() - if lines.find(b"status code 401 Unauthorized") != -1 or lines.find(b"unable to find image") != -1: - bom = dict(remote_info=401) - else: - bom.update({ "targetContainerAppName": img["targetContainerAppName"] }) + if (img["imageName"].find("run-dotnet") != -1) and (img["imageName"].find("cbl-mariner") != -1): + bom = { "remote_info": { "run_images": [{ "name": "mcr.microsoft.com/oryx/builder:" + img["imageName"].split(":")[-1] }] }, "image_name": img["imageName"], "targetContainerName": img["targetContainerName"], "targetContainerAppName": img["targetContainerAppName"], "revisionMode": img["revisionMode"] } + else: + subprocess.run("pack inspect-image " + img["imageName"] + " --output json > ./bom.json 2>&1", shell=True) + with open("./bom.json", "rb") as f: + bom = None + lines = f.read() + if lines.find(b"status code 401 Unauthorized") != -1 or lines.find(b"unable to find image") != -1: + bom = dict(remote_info=401, image_name=img["imageName"]) + else: + bom = json.loads(lines) + bom.update({ "targetContainerName": img["targetContainerName"], "targetContainerAppName": img["targetContainerAppName"], "revisionMode": img["revisionMode"] }) boms.append(bom) - ## For testing - # + # # For testing # with open("./bom.json", "rb") as f: # lines = f.read() # # if lines.find(b"status code 401 Unauthorized") == -1 or lines.find(b"unable to find image") == -1: # # bom = dict(remote_info=401) # # else: - # bom = json.loads(lines) - # bom.update({ "targetContainerAppName": "test-containerapp-1" }) + # bom = json.loads(lines) + # bom.update({ "targetContainerName": "test-containerapp-1" }) # boms.append(bom) # Get the current tags of Dotnet Mariners @@ -4354,17 +4347,16 @@ def patch_list(cmd, resource_group_name, managed_env=None, show_all=False): failedReason = "Failed to get BOM of the image. Please check if the image exists or you have the permission to access the image." notBasedMarinerReason = "Image not based on Mariner" mcrCheckReason = "Image not from mcr.microsoft.com/oryx/builder" - results = [] + results = {"NotPatchable": []} # Start checking if the images are based on Mariner for bom in boms: if bom["remote_info"] == 401: - results.append(dict(targetContainerAppName=bom["targetContainerAppName"], oldRunImage=None, newRunImage=None, id=None, reason=failedReason)) + results["NotPatchable"].append(dict(targetContainerName=bom["targetContainerName"], targetContainerAppName=bom["targetContainerAppName"], revisionMode=bom["revisionMode"], targetImageName=bom["image_name"], oldRunImage=None, newRunImage=None, id=None, reason=failedReason)) else: # devide run-images into different parts by "/" runImagesProps = bom["remote_info"]["run_images"] if runImagesProps is None: - - results.append(dict(targetContainerAppName=bom["targetContainerAppName"], oldRunImage=None, newRunImage=None, id=None, reason=notBasedMarinerReason)) + results["NotPatchable"].append(dict(targetContainerName=bom["targetContainerName"], targetContainerAppName=bom["targetContainerAppName"], revisionMode=bom["revisionMode"], targetImageName=bom["image_name"], oldRunImage=None, newRunImage=None, id=None, reason=notBasedMarinerReason)) else: for runImagesProp in runImagesProps: # result = None @@ -4373,14 +4365,65 @@ def patch_list(cmd, resource_group_name, managed_env=None, show_all=False): runImagesTag = runImagesProp[1] # Based on Mariners if runImagesTag.find('mariner') != -1: - results.append(patchableCheck(runImagesTag, oryxRunImgTags, bom=bom)) + checkResult = patchableCheck(runImagesTag, oryxRunImgTags, bom=bom) + if checkResult["id"] == None: + results["NotPatchable"].append(checkResult) + else: + results[checkResult["id"]] = checkResult else: - results.append(dict(targetContainerAppName=bom["targetContainerAppName"], oldRunImage=bom["remote_info"]["run_images"], newRunImage=None, id=None, reason=failedReason)) + results["NotPatchable"].append(dict(targetContainerName=bom["targetContainerName"], targetContainerAppName=bom["targetContainerAppName"], revisionMode=bom["revisionMode"], targetImageName=bom["image_name"], oldRunImage=bom["remote_info"]["run_images"], newRunImage=None, id=None, reason=failedReason)) else: # Not based on image from mcr.microsoft.com/dotnet - results.append(dict(targetContainerAppName=bom["targetContainerAppName"], oldRunImage=bom["remote_info"]["run_images"], newRunImage=None, id=None, reason=mcrCheckReason)) + results["NotPatchable"].append(dict(targetContainerName=bom["targetContainerName"], targetContainerAppName=bom["targetContainerAppName"], revisionMode=bom["revisionMode"], targetImageName=bom["image_name"], oldRunImage=bom["remote_info"]["run_images"], newRunImage=None, id=None, reason=mcrCheckReason)) if show_all == False : - results = [x for x in results if x["newRunImage"] != None] - if results == []: - return "No containerapps available to patch at this time." - return results \ No newline at end of file + results = {k: v for k, v in results.items() if k != "NotPatchable"} + return results + +def patch_run(cmd, resource_group_name, managed_env, show_all=False): + patchable_check_results = patch_list(cmd, resource_group_name, managed_env, show_all=show_all) + patchable_check_results_json = json.dumps(patchable_check_results, indent=4) + print(patchable_check_results_json) + user_input=input("Do you want to apply all the patch or specify by id? (y/n/id)\n") + return patch_apply(cmd, patchable_check_results, user_input) + +def patch_apply(cmd, patchCheckList, method): + results = [] + m = method.strip().lower() + if m == "y": + for key in patchCheckList.keys(): + if key != "NotPatchable": + if patchCheckList[key]["newRunImage"]: + results.append(patch_cli_call(cmd, patchCheckList[key]["targetContainerAppName"], patchCheckList[key]["targetContainerName"], patchCheckList[key]["targetImageName"], patchCheckList[key]["newRunImage"], patchCheckList[key]["revisionMode"])) + elif m == "n": + print("No patch applied.") + else: + if method in patchCheckList.keys(): + results.append(patch_cli_call(cmd, patchCheckList[method]["targetContainerAppName"], patchCheckList[method]["targetContainerName"], patchCheckList[method]["targetImageName"], patchCheckList[method]["newRunImage"], patchCheckList[method]["revisionMode"])) + else: + print("Invalid patch method or id.") + +def patch_cli_call(cmd, resource_group, container_app_name, container_name, target_image_name, new_run_image, revision_mode): + try: + print("Applying patch and publishing...") + subprocess.run(f"pack rebase {target_image_name} --run-image {new_run_image}", shell=True) + new_target_image_name = target_image_name.split(":")[0] + ":" + new_run_image.split(":")[1] + subprocess.run(f"docker tag {target_image_name} {new_target_image_name}", shell=True) + subprocess.run(f"docker push {new_target_image_name}", shell=True) + print("Patch applied and published successfully.\nNew image: " + new_target_image_name) + except Exception: + print("Error: Failed to apply patch and publish. Check if registry is logged in and has write access.") + raise + try: + print("Patching container app: " + container_app_name + " container: " + container_name + " with image: " + new_target_image_name) + update_info_json = update_containerapp(cmd, + name=container_app_name, + resource_group_name=resource_group, + container_name=container_name, + new_image_name=new_target_image_name) + print("Container app revision created successfully.") + update_info = json.loads(update_info_json) + new_revision_info = activate_revision(cmd, resource_group, revision_name=update_info["latestRevisionName"], name=container_app_name) + return new_revision_info + except Exception: + print("Error: Failed to create new revision with the container app.") + raise diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 4b4409cc514..f4d46d94f77 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -8,6 +8,11 @@ from codecs import open from setuptools import setup, find_packages + +import os +import platform +import requests + try: from azure_bdist_wheel import cmdclass except ImportError: @@ -37,9 +42,40 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [ - 'pycomposefile>=0.0.29' + 'pycomposefile>=0.0.29', + 'docker' ] +# Install pack CLI to build runnable application images from source +try: + dir_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "azext_containerapp") + bin_folder = dir_path + "/bin" + if not os.path.exists(bin_folder): + os.makedirs(bin_folder) + + exec_name = "" + host_os = platform.system() + if host_os == "Windows": + exec_name = "pack-v0.29.0-windows.exe" + elif host_os == "Linux": + exec_name = "pack-v0.29.0-linux" + elif host_os == "Darwin": + exec_name = "pack-v0.29.0-macos" + else: + raise Exception(f"Unsupported host OS: {host_os}") + + exec_path = os.path.join(bin_folder, exec_name) + if not os.path.exists(exec_path): + url = f"https://cormteststorage.blob.core.windows.net/pack/{exec_name}" + r = requests.get(url) + with open(exec_path, "wb") as f: + f.write(r.content) + print(f"Successfully installed pack CLI to {exec_path}\n") + +except Exception as e: + # Swallow any exceptions thrown when attempting to install pack CLI + print(f"Failed to install pack CLI: {e}\n") + with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() with open('HISTORY.rst', 'r', encoding='utf-8') as f: diff --git a/src/index.json b/src/index.json index c50ed1e44c7..f0b98d47d66 100644 --- a/src/index.json +++ b/src/index.json @@ -16113,6 +16113,85 @@ "version": "0.21.1" }, "sha256Digest": "fe3140351a6d7a630aba2473403de49b6f8f4af6b67d358396b659cb4fdfd64a" + }, + { + "downloadUrl": "https://github.com/Azure/azure-iot-cli-extension/releases/download/v0.21.2/azure_iot-0.21.2-py3-none-any.whl", + "filename": "azure_iot-0.21.2-py3-none-any.whl", + "metadata": { + "azext.minCliCoreVersion": "2.37.0", + "classifiers": [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: MIT License" + ], + "extensions": { + "python.details": { + "contacts": [ + { + "email": "iotupx@microsoft.com", + "name": "Microsoft", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/azure/azure-iot-cli-extension" + } + } + }, + "extras": [ + "uamqp" + ], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "azure-iot", + "requires_python": ">=3.7", + "run_requires": [ + { + "requires": [ + "azure-core (<2.0.0,>=1.24.0)", + "azure-identity (<2.0.0,>=1.6.1)", + "azure-iot-device (~=2.11)", + "azure-mgmt-core (<2.0.0,>=1.3.0)", + "azure-storage-blob (<13.0.0,>=12.14.0)", + "jsonschema (~=3.2.0)", + "msrest (>=0.6.21)", + "msrestazure (<2.0.0,>=0.6.3)", + "packaging", + "tomli (~=2.0)", + "tomli-w (~=1.0)", + "tqdm (~=4.62)", + "treelib (~=1.6)" + ] + }, + { + "extra": "uamqp", + "requires": [ + "uamqp (~=1.2)" + ] + }, + { + "environment": "python_version < \"3.8\"", + "requires": [ + "importlib-metadata" + ] + } + ], + "summary": "The Azure IoT extension for Azure CLI.", + "version": "0.21.2" + }, + "sha256Digest": "1f3241199456d299b17e2875ef9888de4c2977ddcfd0a3ae72087c9ff389b730" } ], "azurestackhci": [ @@ -38620,6 +38699,78 @@ "version": "2.15.1" }, "sha256Digest": "b195ed8e627c36a34a7aac6f7c261a80714cf7ae9fbfd01981a39119dd1a70a5" + }, + { + "downloadUrl": "https://azuremlsdktestpypi.blob.core.windows.net/wheels/sdk-cli-v2-public/ml-2.16.0-py3-none-any.whl", + "filename": "ml-2.16.0-py3-none-any.whl", + "metadata": { + "azext.minCliCoreVersion": "2.15.0", + "classifiers": [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Environment :: Console", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: MIT License" + ], + "description_content_type": "text/x-rst", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azuremlsdk@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azureml-examples" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "ml", + "run_requires": [ + { + "requires": [ + "azure-common (<2.0.0,>=1.1)", + "azure-mgmt-resource (<23.0.0,>=3.0.0)", + "azure-mgmt-resource (<23.0.0,>=3.0.0)", + "azure-mgmt-resourcegraph (<9.0.0,>=2.0.0)", + "azure-mgmt-resourcegraph (<9.0.0,>=2.0.0)", + "azure-storage-blob (<13.0.0,>=12.10.0)", + "azure-storage-file-datalake (<13.0.0)", + "azure-storage-file-share (<13.0.0)", + "colorama (<0.5.0)", + "cryptography", + "docker", + "isodate", + "jsonschema (<5.0.0,>=4.0.0)", + "marshmallow (<4.0.0,>=3.5)", + "opencensus-ext-azure (<2.0.0)", + "pydash (<6.0.0)", + "pyjwt (<3.0.0)", + "strictyaml (<2.0.0)", + "tqdm (<5.0.0)", + "typing-extensions (<5.0.0)" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools AzureMachineLearningWorkspaces Extension", + "version": "2.16.0" + }, + "sha256Digest": "d86047e774ac8c16b3d6eb4490588b527a257a04977dd181cfd06ac7778b6c23" } ], "mobile-network": [