diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py index 96144b2d929..537304b2ab5 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -4,8 +4,14 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=consider-using-f-string +import os +import json +from datetime import datetime + from azure.cli.core.util import open_page_in_browser +from azure.cli.core.auth.persistence import SecretStore, build_persistence from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) +from ._utils import repo_url_to_name from knack.log import get_logger logger = get_logger(__name__) @@ -25,6 +31,43 @@ ] +def _get_github_token_secret_store(cmd): + location = os.path.join(cmd.cli_ctx.config.config_dir, "github_token_cache") + file_persistence = build_persistence(location, encrypt=True) + return SecretStore(file_persistence) + + +def cache_github_token(cmd, token, repo): + repo = repo_url_to_name(repo) + secret_store = _get_github_token_secret_store(cmd) + cache = secret_store.load() + + for entry in cache: + if isinstance(entry, dict) and entry.get("value") == token: + if repo not in entry.get("repos", []): + entry["repos"] = [*entry.get("repos", []), repo] + entry["last_modified_timestamp"] = datetime.utcnow().timestamp() + break + else: + cache_entry = {"last_modified_timestamp": datetime.utcnow().timestamp(), "value": token, "repos": [repo]} + cache = [cache_entry, *cache] + + secret_store.save(cache) + + +def load_github_token_from_cache(cmd, repo): + repo = repo_url_to_name(repo) + secret_store = _get_github_token_secret_store(cmd) + cache = secret_store.load() + + if isinstance(cache, list): + for entry in cache: + if isinstance(entry, dict) and repo in entry.get("repos", []): + return entry.get("value") + + return None + + def get_github_access_token(cmd, scope_list=None, token=None): # pylint: disable=unused-argument if token: return token diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index a54fd75f90e..464ac0b7ba1 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -57,6 +57,8 @@ create_or_update_github_action, ) +from ._github_oauth import load_github_token_from_cache, get_github_access_token + logger = get_logger(__name__) @@ -867,3 +869,14 @@ def check_env_name_on_rg(cmd, managed_env, resource_group_name, location): if env_def: if location != env_def["location"]: raise ValidationError("Environment {} already exists in resource group {} on location {}, cannot change location of existing environment to {}.".format(parse_resource_id(managed_env)["name"], resource_group_name, env_def["location"], location)) + + +def get_token(cmd, repo, token): + if not repo: + return None + if token: + return token + token = load_github_token_from_cache(cmd, repo) + if not token: + token = get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) + return token diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index eb44906c87e..60eebe52652 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -173,7 +173,7 @@ def get_workflow(github_repo, name): # pylint: disable=inconsistent-return-stat workflows = list(github_repo.get_workflows()) workflows.sort(key=lambda r: r.created_at, reverse=True) # sort by latest first for wf in workflows: - if wf.path.startswith(f".github/workflows/{name}") and "Trigger auto deployment for containerapp" in wf.name: + if wf.path.startswith(f".github/workflows/{name}") and f"Trigger auto deployment for" in wf.name: return wf @@ -244,7 +244,7 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim def repo_url_to_name(repo_url): repo = None - repo = repo_url.split('/') + repo = [s for s in repo_url.split('/') if s] if len(repo) >= 2: repo = '/'.join(repo[-2:]) return repo diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c6058e761ac..b0254beba85 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2015,7 +2015,8 @@ 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, AzureContainerRegistry, - check_env_name_on_rg) + check_env_name_on_rg, get_token) + from ._github_oauth import cache_github_token, load_github_token_from_cache HELLOWORLD = "mcr.microsoft.com/azuredocs/containerapps-helloworld" dockerfile = "Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) @@ -2024,7 +2025,7 @@ def containerapp_up(cmd, check_env_name_on_rg(cmd, managed_env, resource_group_name, location) image = _reformat_image(source, repo, image) - token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) + token = get_token(cmd, repo, token) if image and HELLOWORLD in image.lower(): ingress = "external" if not ingress else ingress @@ -2064,6 +2065,7 @@ def containerapp_up(cmd, if repo: _create_github_action(app, env, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id, branch, token, repo, context_path) + cache_github_token(cmd, token, repo) if browse: open_containerapp_in_browser(cmd, app.name, app.resource_group.name)