diff --git a/src/containerapp/.gitignore b/src/containerapp/.gitignore new file mode 100644 index 0000000000..d79675e946 --- /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/_help.py b/src/containerapp/azext_containerapp/_help.py index ac20bfd038..7b1050d5b7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -1271,3 +1271,15 @@ --environment MyContainerappEnv \\ --compose-file-path "path/to/docker-compose.yml" """ + +helps['containerapp patch list'] = """ + type: command + short-summary: List patchable and unpatchable container apps. + examples: + - name: List patchable container apps. + text: | + az containerapp list -g MyResourceGroup --environment MyContainerappEnv + - name: List patchable and non-patchable container apps. + text: | + az containerapp list -g MyResourceGroup --environment MyContainerappEnv --show-all +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index ae9720c8e3..fb75049831 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -293,3 +293,26 @@ "validationMethod": None # str } } + +# ContainerApp Patch +ImageProperties = { + "imageName": None, + "targetContainerAppName": None +} + +ImagePatchableCheck = { + "targetContainerAppName": None, + "oldRunImage": None, + "newRunImage": None, + "id": None, + "reason": None +} + +OryxMarinerRunImgTagProperty = { + "fullTag": None, + "framework": None, + "version": None, + "marinerVersion": None, + "architectures": None, + "support": None, +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0fe3852be6..d59a3c9618 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') @@ -414,3 +414,8 @@ def load_arguments(self, _): c.argument('workload_profile_type', help="The type of workload profile to add or update. Run 'az containerapp env workload-profile list-supported -l ' to check the options for your region.") c.argument('min_nodes', help="The minimum node count for the workload profile") c.argument('max_nodes', help="The maximum node count for the workload profile") + + 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') \ 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 11034fbcdb..bb2da6db39 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 8bcc296a23..74b1cbf01f 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -7,6 +7,12 @@ import time import json import platform +import docker +import os +import requests +import hashlib +import packaging.version as SemVer +import re from urllib.parse import urlparse from datetime import datetime @@ -31,6 +37,7 @@ LOG_ANALYTICS_RP, CONTAINER_APPS_RP, CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE, ACR_IMAGE_SUFFIX, LOGS_STRING, PENDING_STATUS, SUCCEEDED_STATUS, UPDATING_STATUS) from ._models import (ContainerAppCustomDomainEnvelope as ContainerAppCustomDomainEnvelopeModel, ManagedCertificateEnvelop as ManagedCertificateEnvelopModel) +from ._models import ImagePatchableCheck, OryxMarinerRunImgTagProperty logger = get_logger(__name__) @@ -1714,3 +1721,136 @@ def format_location(location=None): if location: 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["oldRunImage"] = repoTagSplit + result["reason"] = "Image not based on dotnet Mariner." + 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"]] + + # Check if the image minor version is four less than the latest minor version + if tagProp["version"] < matchingVersionInfo[0]["version"]: + result = ImagePatchableCheck + result["targetContainerAppName"] = bom["targetContainerAppName"] + 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["reason"] = "New security patch released for your current run image." + else: + # Not patchable + result["newRunImage"] = "mcr.microsoft.com/oryx/builder:" + matchingVersionInfo[0]["fullTag"] + result["reason"] = "The image is not pachable Please check for major or minor version upgrade." + else: + result = ImagePatchableCheck + result["targetContainerAppName"] = bom["targetContainerAppName"] + result["oldRunImage"] = tagProp["fullTag"] + result["reason"] = "You're already up to date!" + return result + + +def getCurrentMarinerTags() -> list(OryxMarinerRunImgTagProperty): + r = requests.get("https://mcr.microsoft.com/v2/oryx/builder/tags/list") + tags = r.json() + # tags = dict(tags=["run-dotnet-aspnet-7.0.1-cbl-mariner2.0", "run-dotnet-aspnet-7.0.1-cbl-mariner1.0", "run-dotnet-aspnet-7.1.0-cbl-mariner2.0"]) + tagList = {} + # only keep entries that container keyword "mariner" + tags = [tag for tag in tags["tags"] if "mariner" in tag] + for tag in tags: + tagObj = parseOryxMarinerTag(tag) + if tagObj: + majorMinorVer = str(tagObj["version"].major) + "." + str(tagObj["version"].minor) + support = tagObj["support"] + framework = tagObj["framework"] + marinerVer = tagObj["marinerVersion"] + if framework in tagList.keys(): + if majorMinorVer in tagList[framework].keys(): + if support in tagList[framework][majorMinorVer].keys(): + if marinerVer in tagList[framework][majorMinorVer][support].keys(): + tagList[framework][majorMinorVer][support][marinerVer].append(tagObj) + tagList[framework][majorMinorVer][support][marinerVer].sort(reverse=True, key=lambda x: x["version"]) + else: + tagList[framework][majorMinorVer][support][marinerVer] = [tagObj] + else: + tagList[framework][majorMinorVer][support] = {marinerVer: [tagObj]} + else: + tagList[framework][majorMinorVer] = {support: {marinerVer: [tagObj]}} + else: + tagList[framework] = {majorMinorVer: {support: {marinerVer: [tagObj]}}} + return tagList + + +def parseOryxMarinerTag(tag: str) -> OryxMarinerRunImgTagProperty: + tagSplit = tag.split("-") + if tagSplit[0] == "run" and tagSplit[1] == "dotnet": + versionRE = r"(\d+\.\d+(\.\d+)?).*?(cbl-mariner(\d+\.\d+))" + REmatches = re.findall(versionRE, tag) + if REmatches.count == 0: + tagObj = None + else: + tagObj = dict(fullTag=tag, version=SemVer.parse(REmatches[0][0]), framework=tagSplit[2], marinerVersion=REmatches[0][2], architectures=None, support="lts") + else: + tagObj = None + return tagObj diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 0a4a359d77..7a311631b4 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -199,3 +199,6 @@ def load_command_table(self, _): g.custom_show_command('show', 'show_workload_profile') g.custom_command('set', 'set_workload_profile') g.custom_command('delete', 'delete_workload_profile') + + with self.command_group('containerapp patch', is_preview=True) as g: + g.custom_command('list', 'patch_list') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a269de1da5..7565266ba5 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -9,6 +9,8 @@ import time from urllib.parse import urlparse import requests +import json +import subprocess from azure.cli.core.azclierror import ( RequiredArgumentMissingError, @@ -57,6 +59,7 @@ ScaleRule as ScaleRuleModel, Volume as VolumeModel, VolumeMount as VolumeMountModel,) +from ._models import OryxMarinerRunImgTagProperty, ImagePatchableCheck, ImageProperties from ._utils import (_validate_subscription_registered, _ensure_location_allowed, parse_secret_flags, store_as_secret_and_return_secret_ref, parse_env_var_flags, @@ -73,7 +76,8 @@ validate_environment_location, safe_set, parse_metadata_flags, parse_auth_flags, _azure_monitor_quickstart, set_ip_restrictions, certificate_location_matches, certificate_matches, generate_randomized_managed_cert_name, check_managed_cert_name_availability, prepare_managed_certificate_envelop, - get_default_workload_profile_name_from_env, get_default_workload_profiles, ensure_workload_profile_supported, _generate_secret_volume_name) + get_default_workload_profile_name_from_env, get_default_workload_profiles, ensure_workload_profile_supported, _generate_secret_volume_name, + getCurrentMarinerTags, patchableCheck) from ._validators import validate_create, validate_revision_suffix from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) @@ -4129,8 +4133,7 @@ def show_auth_config(cmd, resource_group_name, name): return auth_settings # Compose - - + def create_containerapps_from_compose(cmd, # pylint: disable=R0914 resource_group_name, managed_env, @@ -4297,3 +4300,76 @@ def delete_workload_profile(cmd, resource_group_name, env_name, workload_profile return r except Exception as e: handle_raw_exception(e) + +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"]) + imgs.append(result) + + # Get the BOM of the images + results = [] + boms = [] + + ## 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 = json.loads(lines) + bom.update({ "targetContainerAppName": img["targetContainerAppName"] }) + boms.append(bom) + + ## 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" }) + # boms.append(bom) + + # Get the current tags of Dotnet Mariners + oryxRunImgTags = getCurrentMarinerTags() + 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 = [] + # 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)) + 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)) + else: + for runImagesProp in runImagesProps: + # result = None + if (runImagesProp["name"].find("mcr.microsoft.com/oryx/builder") != -1): + runImagesProp = runImagesProp["name"].split(":") + runImagesTag = runImagesProp[1] + # Based on Mariners + if runImagesTag.find('mariner') != -1: + results.append(patchableCheck(runImagesTag, oryxRunImgTags, bom=bom)) + else: + results.append(dict(targetContainerAppName=bom["targetContainerAppName"], 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)) + if show_all == False : + results = [x for x in results if x["newRunImage"] != None] + return results \ No newline at end of file diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 4b4409cc51..f4d46d94f7 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 c50ed1e44c..f0b98d47d6 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": [