diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index f5ec379f1a7..0bf3db8acfe 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -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 ++++++ diff --git a/src/containerapp/azext_containerapp/_constants.py b/src/containerapp/azext_containerapp/_constants.py index 97d91b76bdd..b455e6a7184 100644 --- a/src/containerapp/azext_containerapp/_constants.py +++ b/src/containerapp/azext_containerapp/_constants.py @@ -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" + diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 7bf0dbad415..e202033393a 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -6,7 +6,6 @@ from knack.help_files import helps # pylint: disable=unused-import - helps['containerapp'] = """ type: group short-summary: Manage Azure Container Apps. @@ -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 diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 6947bbba79e..312af07f2e9 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -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, _): @@ -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.') diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 2c904f8810d..e578e3a97db 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -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 @@ -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 ( @@ -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, @@ -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 @@ -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): @@ -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: @@ -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", @@ -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" diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a652b518660..26ee88f41be 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -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 "" + 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: @@ -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) @@ -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 ` 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) @@ -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: @@ -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): diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index c36f9c504f8..bc78513c85a 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -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 = [