Skip to content

Commit

Permalink
Merge pull request Azure#139 from StrawnSC/no-dockerfile
Browse files Browse the repository at this point in the history
`az containerapp up`: Support No Dockerfile Scenario
  • Loading branch information
StrawnSC authored Aug 31, 2022
2 parents 6203862 + e167f66 commit 8475e28
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Release History

0.3.11
++++++

* 'az containerapp up': autogenerate a docker container with --source when no dockerfile present

0.3.10
++++++
Expand Down
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,16 @@
NAME_INVALID = "Invalid"
NAME_ALREADY_EXISTS = "AlreadyExists"

ACR_TASK_TEMPLATE = """version: v1.1.0
steps:
- cmd: mcr.microsoft.com/oryx/cli:20220811.1 oryx dockerfile --bind-port {{target_port}} --output ./Dockerfile .
timeout: 28800
- build: -t $Registry/{{image_name}} -f Dockerfile .
timeout: 28800
- push: ["$Registry/{{image_name}}"]
timeout: 1800
"""
DEFAULT_PORT = 8080 # used for no dockerfile scenario; not the hello world image

HELLO_WORLD_IMAGE = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"

3 changes: 1 addition & 2 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from knack.help_files import helps # pylint: disable=unused-import


helps['containerapp'] = """
type: group
short-summary: Manage Azure Container Apps.
Expand Down Expand Up @@ -117,7 +116,7 @@
- name: Create a container app from a dockerfile in a GitHub repo (setting up github actions)
text: |
az containerapp up -n MyContainerapp --repo https://github.com/myAccount/myRepo
- name: Create a container app from a dockerfile in a local directory
- name: Create a container app from a dockerfile in a local directory (or autogenerate a container if no dockerfile is found)
text: |
az containerapp up -n MyContainerapp --source .
- name: Create a container app from an image in a registry
Expand Down
4 changes: 2 additions & 2 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ._validators import (validate_memory, validate_cpu, validate_managed_env_name_or_id, validate_registry_server,
validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress)
from ._constants import UNAUTHENTICATED_CLIENT_ACTION, FORWARD_PROXY_CONVENTION, MAXIMUM_CONTAINER_APP_NAME_LENGTH
from ._constants import UNAUTHENTICATED_CLIENT_ACTION, FORWARD_PROXY_CONVENTION, MAXIMUM_CONTAINER_APP_NAME_LENGTH, DEFAULT_PORT


def load_arguments(self, _):
Expand Down Expand Up @@ -267,7 +267,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 to upload to Azure container registry.')
c.argument('source', help=f'Local directory path to upload to Azure container registry. If no dockerfile is present (called "Dockerfile" and in the project root), Oryx will be used to create a docker container based on the directory contents (with a default target port of {DEFAULT_PORT}). 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.')

Expand Down
88 changes: 76 additions & 12 deletions src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals, logging-fstring-interpolation, arguments-differ, abstract-method, logging-format-interpolation, broad-except


from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
import requests

Expand All @@ -13,6 +14,7 @@
ValidationError,
InvalidArgumentValueError,
MutuallyExclusiveArgumentError,
CLIError,
)
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.command_modules.appservice._create_util import (
Expand Down Expand Up @@ -47,7 +49,13 @@
validate_environment_location
)

from ._constants import MAXIMUM_SECRET_LENGTH, LOG_ANALYTICS_RP, CONTAINER_APPS_RP, ACR_IMAGE_SUFFIX, MAXIMUM_CONTAINER_APP_NAME_LENGTH
from ._constants import (MAXIMUM_SECRET_LENGTH,
LOG_ANALYTICS_RP,
CONTAINER_APPS_RP,
ACR_IMAGE_SUFFIX,
MAXIMUM_CONTAINER_APP_NAME_LENGTH,
ACR_TASK_TEMPLATE,
DEFAULT_PORT)

from .custom import (
create_managed_environment,
Expand Down Expand Up @@ -314,7 +322,46 @@ def create_acr(self):
self.cmd.cli_ctx, registry_name
)

def run_acr_build(self, dockerfile, source, quiet=False):
def build_container_from_source(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

task_name = "cli_build_containerapp"
registry_name = (self.registry_server[: self.registry_server.rindex(ACR_IMAGE_SUFFIX)]).lower()
if not self.target_port:
self.target_port = DEFAULT_PORT
task_content = ACR_TASK_TEMPLATE.replace("{{image_name}}", image_name).replace("{{target_port}}", str(self.target_port))
task_client = cf_acr_tasks(self.cmd.cli_ctx)
run_client = cf_acr_runs(self.cmd.cli_ctx)
task_command_kwargs = {"resource_type": ResourceType.MGMT_CONTAINERREGISTRY, 'operation_group': 'webhooks'}
old_command_kwargs = {}
for key in task_command_kwargs:
old_command_kwargs[key] = self.cmd.command_kwargs.get(key)
self.cmd.command_kwargs[key] = task_command_kwargs[key]

with NamedTemporaryFile(mode="w") as task_file:
task_file.write(task_content)
task_file.flush()

acr_task_create(self.cmd, task_client, task_name, registry_name, context_path="/dev/null", file=task_file.name)
logger.warning("Created ACR task %s in registry %s", task_name, registry_name)
from time import sleep
sleep(10)

logger.warning("Running ACR build...")
try:
acr_task_run(self.cmd, run_client, task_name, registry_name, file=task_file.name, context_path=source)
except CLIError as e:
logger.error("Failed to automatically generate a docker container from your source. \n"
"See the ACR logs above for more error information. \nPlease check the supported langauges for autogenerating docker containers (https://github.com/microsoft/Oryx/blob/main/doc/supportedRuntimeVersions.md), "
"or consider using a Dockerfile for your app.")
raise e

for k, v in old_command_kwargs.items():
self.cmd.command_kwargs[k] = v

def run_acr_build(self, dockerfile, source, quiet=False, build_from_source=False):
image_name = self.image if self.image is not None else self.name
from datetime import datetime

Expand All @@ -326,15 +373,21 @@ def run_acr_build(self, dockerfile, source, quiet=False):

self.image = self.registry_server + "/" + image_name

queue_acr_build(
self.cmd,
self.acr.resource_group.name,
self.acr.name,
image_name,
source,
dockerfile,
quiet,
)

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)
else:
queue_acr_build(
self.cmd,
self.acr.resource_group.name,
self.acr.name,
image_name,
source,
dockerfile,
quiet,
)


def _create_service_principal(cmd, resource_group_name, env_resource_group_name):
Expand Down Expand Up @@ -480,6 +533,14 @@ def _reformat_image(source, repo, image):
return image


def _has_dockerfile(source, dockerfile):
try:
content = _get_dockerfile_content_local(source, dockerfile)
return bool(content)
except InvalidArgumentValueError:
return False


def _get_dockerfile_content_local(source, dockerfile):
lines = []
if source:
Expand Down Expand Up @@ -772,7 +833,7 @@ def _create_github_action(
)


def up_output(app):
def up_output(app: 'ContainerApp', no_dockerfile):
url = safe_get(
ContainerAppClient.show(app.cmd, app.resource_group.name, app.name),
"properties",
Expand All @@ -786,6 +847,9 @@ def up_output(app):
logger.warning(
f"\nYour container app {app.name} has been created and deployed! Congrats! \n"
)
if no_dockerfile and app.ingress:
logger.warning(f"Your app is running image {app.image} and listening on port {app.target_port}")

url and logger.warning(f"Browse to your container app at: {url} \n")
logger.warning(
f"Stream logs for your container with: az containerapp logs show -n {app.name} -g {app.resource_group.name} \n"
Expand Down
18 changes: 12 additions & 6 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,10 @@ def create_containerapp(cmd,
if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]:
not disable_warnings and logger.warning("\nContainer app created. Access your app at https://{}/\n".format(r["properties"]["configuration"]["ingress"]["fqdn"]))
else:
not disable_warnings and logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: az containerapp ingress enable --help\n")
target_port = target_port or "<port>"
not disable_warnings and logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: "
"az containerapp ingress enable -n %s -g %s --type external --target-port %s"
" --transport auto\n", name, resource_group_name, target_port)

return r
except Exception as e:
Expand Down Expand Up @@ -2328,7 +2331,7 @@ def containerapp_up(cmd,
from ._up_utils import (_validate_up_args, _reformat_image, _get_dockerfile_content, _get_ingress_and_target_port,
ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app,
_get_registry_details, _create_github_action, _set_up_defaults, up_output,
check_env_name_on_rg, get_token, _validate_containerapp_name)
check_env_name_on_rg, get_token, _validate_containerapp_name, _has_dockerfile)
from ._github_oauth import cache_github_token
HELLOWORLD = "mcr.microsoft.com/azuredocs/containerapps-helloworld"
dockerfile = "Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated)
Expand All @@ -2352,8 +2355,11 @@ def containerapp_up(cmd,
target_port = 80
logger.warning("No ingress provided, defaulting to port 80. Try `az containerapp up --ingress %s --target-port <port>` to set a custom port.", ingress)

dockerfile_content = _get_dockerfile_content(repo, branch, token, source, context_path, dockerfile)
ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content)
if source and not _has_dockerfile(source, dockerfile):
pass
else:
dockerfile_content = _get_dockerfile_content(repo, branch, token, source, context_path, dockerfile)
ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content)

resource_group = ResourceGroup(cmd, name=resource_group_name, location=location)
env = ContainerAppEnvironment(cmd, managed_env, resource_group, location=location, logs_key=logs_key, logs_customer_id=logs_customer_id)
Expand All @@ -2376,7 +2382,7 @@ def containerapp_up(cmd,
app.create_acr_if_needed()

if source:
app.run_acr_build(dockerfile, source, False)
app.run_acr_build(dockerfile, source, quiet=False, build_from_source=not _has_dockerfile(source, dockerfile))

app.create(no_registry=bool(repo))
if repo:
Expand All @@ -2387,7 +2393,7 @@ def containerapp_up(cmd,
if browse:
open_containerapp_in_browser(cmd, app.name, app.resource_group.name)

up_output(app)
up_output(app, no_dockerfile=(source and not _has_dockerfile(source, dockerfile)))


def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, env_vars, ingress, target_port, registry_server, registry_user, registry_pass):
Expand Down
1 change: 1 addition & 0 deletions src/containerapp/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

VERSION = '0.3.11'


# The full list of classifiers is available at
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
CLASSIFIERS = [
Expand Down

0 comments on commit 8475e28

Please sign in to comment.