Skip to content

Commit

Permalink
Merge pull request Azure#2 from cormacpayne/corm/containerapp-up-buil…
Browse files Browse the repository at this point in the history
…dpack

Use buildpacks for 'az containerapp up --source' no Dockerfile scenario
  • Loading branch information
daniv-msft committed May 5, 2023
2 parents 2b5cb59 + 3fb96b9 commit e46dd23
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 7 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/*
2 changes: 1 addition & 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
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
62 changes: 60 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,6 +1722,60 @@ 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:
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)
repoTagSplit = repoTagSplit.split("-")
Expand Down Expand Up @@ -1747,6 +1803,7 @@ def patchableCheck(repoTagSplit: str, oryxBuilderRunImgTags, bom):
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()
Expand Down Expand Up @@ -1777,6 +1834,7 @@ def getCurrentMarinerTags() -> list(OryxMarinerRunImgTagProperty):
tagList[framework] = {majorMinorVer: {support: {marinerVer: [tagObj]}}}
return tagList


def parseOryxMarinerTag(tag: str) -> OryxMarinerRunImgTagProperty:
tagSplit = tag.split("-")
if tagSplit[0] == "run" and tagSplit[1] == "dotnet":
Expand All @@ -1788,4 +1846,4 @@ def parseOryxMarinerTag(tag: str) -> OryxMarinerRunImgTagProperty:
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
return tagObj
38 changes: 37 additions & 1 deletion src/containerapp/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit e46dd23

Please sign in to comment.