Skip to content
Closed
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
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@
--image my-app:v1.0 --environment MyContainerappEnv \\
--secrets mysecret=secretvalue1 anothersecret="secret value 2" \\
--secret-volume-mount "mnt/secrets"
- name: Create a container app from a new GitHub Actions workflow in the provided GitHub repository
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--environment MyContainerappEnv --registry-server MyRegistryServer \\
--registry-user MyRegistryUser --registry-pass MyRegistryPass \\
--repo https://github.com/myAccount/myRepo
- name: Create a Container App from the provided application source
text: |
az containerapp create -n MyContainerapp -g MyResourceGroup \\
--environment MyContainerappEnv --registry-server MyRegistryServer \\
--registry-user MyRegistryUser --registry-pass MyRegistryPass \\
--source .
"""

helps['containerapp update'] = """
Expand Down
10 changes: 10 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def load_arguments(self, _):
c.argument('workload_profile_name', options_list=['--workload-profile-name', '-w'], help="Name of the workload profile to run the app on.", is_preview=True)
c.argument('secret_volume_mount', help="Path to mount all secrets e.g. mnt/secrets", is_preview=True)
c.argument('termination_grace_period', type=int, options_list=['--termination-grace-period', '--tgp'], help="Duration in seconds a replica is given to gracefully shut down before it is forcefully terminated. (Default: 30)", is_preview=True)
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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the argument is in preview, add is_preview=True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These flags are not in preview.


with self.argument_context('containerapp create', arg_group='Identity') as c:
c.argument('user_assigned', nargs='+', help="Space-separated user identities to be assigned.")
Expand All @@ -135,6 +136,15 @@ def load_arguments(self, _):
c.argument('service_type', help="The service information for dev services.")
c.ignore('service_type')

with self.argument_context('containerapp create', arg_group='GitHub Repository') as c:
c.argument('repo', help='Create an app via GitHub Actions in the format: https://github.com/<owner>/<repository-name> or <owner>/<repository-name>')
c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line. If not provided or not found in the cache (and using --repo), a browser page will be opened to authenticate with Github.')
c.argument('branch', options_list=['--branch', '-b'], help='Branch in the provided GitHub repository. Assumed to be the GitHub repository\'s default branch if not specified.')
c.argument('context_path', help='Path in the repository to run docker build. Defaults to "./". Dockerfile is assumed to be named "Dockerfile" and in this directory.')
c.argument('service_principal_client_id', help='The service principal client ID. Used by GitHub Actions to authenticate with Azure.', options_list=["--service-principal-client-id", "--sp-cid"])
c.argument('service_principal_client_secret', help='The service principal client secret. Used by GitHub Actions to authenticate with Azure.', options_list=["--service-principal-client-secret", "--sp-sec"])
c.argument('service_principal_tenant_id', help='The service principal tenant ID. Used by GitHub Actions to authenticate with Azure.', options_list=["--service-principal-tenant-id", "--sp-tid"])

with self.argument_context('containerapp show') as c:
c.argument('show_secrets', help="Show Containerapp secrets.", action='store_true')

Expand Down
1 change: 0 additions & 1 deletion src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
LOG_ANALYTICS_RP,
CONTAINER_APPS_RP,
ACR_IMAGE_SUFFIX,
MAXIMUM_CONTAINER_APP_NAME_LENGTH,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: intended change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, MAXIMUM_CONTAINER_APP_NAME_LENGTH is not used anywhere in this file.

ACR_TASK_TEMPLATE,
DEFAULT_PORT)

Expand Down
18 changes: 15 additions & 3 deletions src/containerapp/azext_containerapp/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,33 @@

import re
from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError, InvalidArgumentValueError,
MutuallyExclusiveArgumentError)
MutuallyExclusiveArgumentError, RequiredArgumentMissingError)
from msrestazure.tools import is_valid_resource_id
from knack.log import get_logger

from ._clients import ContainerAppClient
from ._ssh_utils import ping_container_app
from ._utils import safe_get, is_registry_msi_system
from ._constants import ACR_IMAGE_SUFFIX, LOG_TYPE_SYSTEM
from ._constants import ACR_IMAGE_SUFFIX, LOG_TYPE_SYSTEM, MAXIMUM_SECRET_LENGTH
from urllib.parse import urlparse


logger = get_logger(__name__)


# called directly from custom method bc otherwise it disrupts the --environment auto RID functionality
def validate_create(registry_identity, registry_pass, registry_user, registry_server, no_wait):
def validate_create(registry_identity, registry_pass, registry_user, registry_server, no_wait, source=None, repo=None):
if source and repo:
raise MutuallyExclusiveArgumentError("Cannot use --source and --repo together. Can either deploy from a local directory or a GitHub repository")
if source or repo:
if not registry_server or not registry_user or not registry_pass:
raise RequiredArgumentMissingError('Usage error: --registry-server, --registry-username and --registry-password are required while using --source or --repo.')
if repo and registry_server and "azurecr.io" in registry_server:
parsed = urlparse(registry_server)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cormacpayne we should check this condition for source also?

Copy link
Member

@cormacpayne cormacpayne Jul 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snehapar9 we should since we have the condition above to validate that registry_server is provided when source is provided -- we should be able to indent this entire block and remove the repo and registry_server checks to make it straightforward:

if source or repo:
  if not registry_server or not registry_user or not registry_pass:
    raise RequiredArgumentMissingError('Usage error: --registry-server, --registry-username and --registry-password are required while using --source or --repo.')

  if "azurecr.io" in registry_server:
    parsed = urlparse(registry_server)
    registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0]
    if registry_name and len(registry_name) > MAXIMUM_SECRET_LENGTH:
        raise ValidationError(f"--registry-server ACR name must be less than {MAXIMUM_SECRET_LENGTH} "
                              "characters when using --repo")```

registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0]
if registry_name and len(registry_name) > MAXIMUM_SECRET_LENGTH:
raise ValidationError(f"--registry-server ACR name must be less than {MAXIMUM_SECRET_LENGTH} "
"characters when using --repo")
if registry_identity and (registry_pass or registry_user):
raise MutuallyExclusiveArgumentError("Cannot provide both registry identity and username/password")
if is_registry_msi_system(registry_identity) and no_wait:
Expand Down
87 changes: 86 additions & 1 deletion src/containerapp/azext_containerapp/containerapp_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
from ._constants import (CONTAINER_APPS_RP,
HELLO_WORLD_IMAGE)

from ._github_oauth import cache_github_token

logger = get_logger(__name__)


Expand Down Expand Up @@ -168,6 +170,30 @@ def get_argument_no_wait(self):
def get_argument_yaml(self):
return self.get_param("yaml")

def get_argument_source(self):
return self.get_param("source")

def get_argument_repo(self):
return self.get_param("repo")

def get_argument_branch(self):
return self.get_param("branch")

def get_argument_token(self):
return self.get_param("token")

def get_argument_context_path(self):
return self.get_param("context_path")

def get_argument_service_principal_client_id(self):
return self.get_param("service_principal_client_id")

def get_argument_service_principal_client_secret(self):
return self.get_param("service_principal_client_secret")

def get_argument_service_principal_tenant_id(self):
return self.get_param("service_principal_tenant_id")

def get_argument_image(self):
return self.get_param("image")

Expand Down Expand Up @@ -331,7 +357,7 @@ def __init__(

def validate_arguments(self):
validate_container_app_name(self.get_argument_name(), AppType.ContainerApp.name)
validate_create(self.get_argument_registry_identity(), self.get_argument_registry_pass(), self.get_argument_registry_user(), self.get_argument_registry_server(), self.get_argument_no_wait())
validate_create(self.get_argument_registry_identity(), self.get_argument_registry_pass(), self.get_argument_registry_user(), self.get_argument_registry_server(), self.get_argument_no_wait(), self.get_argument_source(), self.get_argument_repo())
validate_revision_suffix(self.get_argument_revision_suffix())

def construct_containerapp(self):
Expand Down Expand Up @@ -541,6 +567,10 @@ def construct_containerapp(self):
else:
set_managed_identity(self.cmd, self.get_argument_resource_group_name(), self.containerapp_def, user_assigned=[self.get_argument_registry_identity()])

if self.get_argument_source():
app = self.set_up_create_containerapp_if_source_or_repo()
self.set_up_create_containerapp_source(app=app)

def create_containerapp(self):
try:
r = self.client.create_or_update(
Expand Down Expand Up @@ -594,6 +624,61 @@ def post_process_containerapp(self, r):
linker_client.linker.begin_create_or_update(resource_uri=r["id"],
parameters=item["parameters"],
linker_name=item["linker_name"]).result()

if self.get_argument_repo():
app = self.set_up_create_containerapp_if_source_or_repo()
r = self.set_up_create_containerapp_repo(app=app, r=r, env=app.env, env_rg=app.resource_group.name)
return r

def set_up_create_containerapp_if_source_or_repo(self):
from ._up_utils import (ContainerApp, ResourceGroup, ContainerAppEnvironment, _reformat_image)

# Parse resource group name and managed env name
env_id = self.containerapp_def["properties"]['environmentId']
parsed_managed_env = parse_resource_id(env_id)
env_name = parsed_managed_env['name']
env_rg = parsed_managed_env['resource_group']

# Parse location
env_info = self.get_environment_client().show(cmd=self.cmd, resource_group_name=env_rg, name=env_name)
location = env_info['location']

# Set image to None if it was previously set to the default image (case where image was not provided by the user) else reformat it
image = None if self.get_argument_image().__eq__(HELLO_WORLD_IMAGE) else _reformat_image(self.get_argument_source(), self.get_argument_repo(), self.get_argument_image())

# Construct ContainerApp
resource_group = ResourceGroup(self.cmd, env_rg, location=location)
env = ContainerAppEnvironment(self.cmd, env_name, resource_group, location=location)
app = ContainerApp(self.cmd, self.get_argument_name(), resource_group, None, image, env, self.get_argument_target_port(), self.get_argument_registry_server(), self.get_argument_registry_user(), self.get_argument_registry_pass(), self.get_argument_env_vars(), self.get_argument_workload_profile_name(), self.get_argument_ingress())

return app

def set_up_create_containerapp_source(self, app):
from ._up_utils import (_get_registry_details, get_token, _has_dockerfile, _get_dockerfile_content, _get_ingress_and_target_port)
dockerfile = "Dockerfile"
token = get_token(self.cmd, self.get_argument_repo(), self.get_argument_token())
_get_registry_details(self.cmd, app, self.get_argument_source()) # fetch ACR creds from arguments registry arguments

if self.get_argument_source() and not _has_dockerfile(self.get_argument_source(), dockerfile):
pass
else:
dockerfile_content = _get_dockerfile_content(self.get_argument_repo(), self.get_argument_branch(), token, self.get_argument_source(), self.get_argument_context_path(), dockerfile)
ingress, target_port = _get_ingress_and_target_port(self.get_argument_ingress(), self.get_argument_target_port(), dockerfile_content)

# Uses buildpacks to generate image if Dockerfile was not provided by the user
app.run_acr_build(dockerfile, self.get_argument_source(), quiet=False, build_from_source=not _has_dockerfile(self.get_argument_source(), dockerfile))

# Update image
self.containerapp_def["properties"]["template"]["containers"][0]["image"] = HELLO_WORLD_IMAGE if app.image is None else app.image

def set_up_create_containerapp_repo(self, app, r, env, env_rg):
from ._up_utils import (_create_github_action, get_token)
# Get GitHub access token
token = get_token(self.cmd, self.get_argument_repo(), self.get_argument_token())
_create_github_action(app, env, self.get_argument_service_principal_client_id(), self.get_argument_service_principal_client_secret(),
self.get_argument_service_principal_tenant_id(), self.get_argument_branch(), token, self.get_argument_repo(), self.get_argument_context_path())
cache_github_token(self.cmd, token, self.get_argument_repo())
r = self.client.show(cmd=self.cmd, resource_group_name=env_rg, name=app.name)
return r

def set_up_create_containerapp_yaml(self, name, file_name):
Expand Down
10 changes: 9 additions & 1 deletion src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,15 @@ def create_containerapp(cmd,
registry_identity=None,
workload_profile_name=None,
termination_grace_period=None,
secret_volume_mount=None):
secret_volume_mount=None,
source=None,
repo=None,
token=None,
branch=None,
context_path=None,
service_principal_client_id=None,
service_principal_client_secret=None,
service_principal_tenant_id=None):
raw_parameters = locals()

containerapp_create_decorator = ContainerAppCreateDecorator(
Expand Down
Loading