Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/*
9 changes: 7 additions & 2 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
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 @@ -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')
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
76 changes: 74 additions & 2 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

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