Skip to content

Commit

Permalink
Update dev fork with latest changes (#1)
Browse files Browse the repository at this point in the history
* Adding patch list

* Added patch list command

* Revert thumbprint arg

* Fix bugs

* Release ML 2.16.0 (Azure#6259)

* Adds IoT Extension Release v0.21.2 (Azure#6264)

* Integrated with Harry

* Use buildpacks for 'az containerapp up --source' no Dockerfile scenario

* Fixed bug

* Fixed command usage text

* Add warning when Docker client cannot be instantiated

---------

Co-authored-by: snehapar9 <snehapar@microsoft.com>
Co-authored-by: Neehar Duvvuri <40341266+needuv@users.noreply.github.com>
Co-authored-by: snehapar9 <108305436+snehapar9@users.noreply.github.com>
Co-authored-by: Ryan K <ryan.k@outlook.com>
Co-authored-by: daniv-msft <48293037+daniv-msft@users.noreply.github.com>
Co-authored-by: Cormac McCarthy <corm@microsoft.com>
  • Loading branch information
7 people committed May 8, 2023
1 parent e8f6316 commit 7deac82
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 8 deletions.
3 changes: 3 additions & 0 deletions src/containerapp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Temporary folders for shared libraries
azext_containerapp/bin/
azext_containerapp/bin/*
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
23 changes: 23 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
7 changes: 6 additions & 1 deletion src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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 <region>' 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')
73 changes: 70 additions & 3 deletions src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
import requests
import subprocess

from azure.cli.core.azclierror import (
RequiredArgumentMissingError,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
140 changes: 140 additions & 0 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Loading

0 comments on commit 7deac82

Please sign in to comment.