diff --git a/build/core/build_center.py b/build/core/build_center.py index 91814e29..c13e4209 100644 --- a/build/core/build_center.py +++ b/build/core/build_center.py @@ -45,7 +45,8 @@ def __init__(self, build_config, process_list, type, arg_config=None): docker_registry = self.build_config['dockerRegistryInfo']['dockerRegistryDomain'], docker_namespace = self.build_config['dockerRegistryInfo']['dockerNameSpace'], docker_username = self.build_config['dockerRegistryInfo']['dockerUserName'], - docker_password = self.build_config['dockerRegistryInfo']['dockerPassword'] + docker_password = self.build_config['dockerRegistryInfo']['dockerPassword'], + managed_identity_id = self.build_config['dockerRegistryInfo']['managedIdentityId'] ) # Initialize graph instance @@ -158,6 +159,10 @@ def build_center(self): def push_center(self): + # Login to Docker registry before pushing images + self.logger.info("Preparing to push images, logging in to Docker registry...") + self.docker_cli.docker_login() + # Find services and map dockfile to services self.construct_graph() diff --git a/build/core/build_utility.py b/build/core/build_utility.py index 28c0385a..68ce7746 100644 --- a/build/core/build_utility.py +++ b/build/core/build_utility.py @@ -27,7 +27,7 @@ class DockerClient: - def __init__(self, docker_registry, docker_namespace, docker_username, docker_password): + def __init__(self, docker_registry, docker_namespace, docker_username, docker_password, managed_identity_id=None): docker_registry = "" if docker_registry == "public" else docker_registry @@ -35,8 +35,10 @@ def __init__(self, docker_registry, docker_namespace, docker_username, docker_pa self.docker_namespace = docker_namespace self.docker_username = docker_username self.docker_password = docker_password + self.managed_identity_id = managed_identity_id - self.docker_login() + # Login is no longer performed automatically during initialization + # Call docker_login() explicitly when needed (e.g., before pushing images) def set_build_cache_type(self, build_nocache=False): @@ -49,8 +51,94 @@ def resolve_image_name(self, image_name): def docker_login(self): - shell_cmd = "docker login -u {0} -p {1} {2}".format(self.docker_username, self.docker_password, self.docker_registry) - execute_shell(shell_cmd) + if self.docker_username and self.docker_password: + shell_cmd = "docker login -u {0} -p {1} {2}".format(self.docker_username, self.docker_password, self.docker_registry) + execute_shell(shell_cmd) + else: + # Check if already logged in to Azure CLI + try: + logger.info("Checking Azure CLI login status...") + subprocess.check_output("az account show", shell=True, stderr=subprocess.STDOUT) + logger.info("Azure CLI is already logged in") + except subprocess.CalledProcessError: + logger.info("Azure CLI not logged in, initiating login...") + if self.managed_identity_id: + # Login with managed identity + # Please NOTE that the managed identity must have the following permissions to access the ACR registry including: + # ------ "Reader" for az show command to check ACR existence + # ------ "AcrPull" for az acr login and docker pull command + # ------ "AcrPush" if you want to push docker images to ACR registry as well + # Determine the type of managed identity identifier + if self.managed_identity_id.startswith('/subscriptions/'): + # Full resource ID format + shell_cmd = "az login --identity --resource-id {0}".format(self.managed_identity_id) + logger.info("Logging in with managed identity (resource-id): {0}".format(self.managed_identity_id)) + else: + # Assume it's a client ID (UUID format) + shell_cmd = "az login --identity --client-id {0}".format(self.managed_identity_id) + logger.info("Logging in with managed identity (client-id): {0}".format(self.managed_identity_id)) + else: + # Interactive login with user account + shell_cmd = "az login" + logger.info("Initiating interactive Azure login...") + execute_shell(shell_cmd) + + # Check if ACR exists in current subscription before attempting login + # Extract registry name without .azurecr.io suffix + registry_name = self.docker_registry.replace('.azurecr.io', '') + check_cmd = "az acr show --name {0}".format(registry_name) + + try: + logger.info("Checking if ACR '{0}' exists in current subscription...".format(registry_name)) + subprocess.run( + check_cmd, + shell=True, + capture_output=True, + text=True, + timeout=30, + check=True + ) + logger.info("ACR '{0}' found in current subscription".format(registry_name)) + except subprocess.TimeoutExpired: + logger.error("Command timed out: {0}".format(check_cmd)) + logger.error("Failed to check ACR existence within 30 seconds") + sys.exit(1) + except subprocess.CalledProcessError as e: + logger.error("ACR '{0}' not found in current subscription".format(registry_name)) + if e.stderr: + logger.error("Error details: {0}".format(e.stderr.strip())) + logger.error("") + logger.error("Please check:") + logger.error(" 1. The ACR registry name is correct") + logger.error(" 2. You are using the correct Azure subscription") + logger.error("") + logger.error("Current subscription can be checked with: az account show") + logger.error("To switch subscription, use: az account set --subscription ") + sys.exit(1) + + # Login to Azure Container Registry + shell_cmd = "az acr login --name {0}".format(registry_name) + try: + logger.info("Logging in to ACR '{0}'...".format(registry_name)) + subprocess.run( + shell_cmd, + shell=True, + capture_output=True, + text=True, + timeout=30, + check=True + ) + logger.info("Successfully logged in to ACR '{0}'".format(registry_name)) + except subprocess.TimeoutExpired: + logger.error("Command timed out: {0}".format(shell_cmd)) + logger.error("ACR login command exceeded 30 seconds timeout") + sys.exit(1) + except subprocess.CalledProcessError as e: + logger.error("Failed to login to ACR '{0}'".format(registry_name)) + if e.stderr: + logger.error("Error details: {0}".format(e.stderr.strip())) + logger.error("Please check your permissions to access this ACR registry") + sys.exit(1) def docker_image_build(self, image_name, dockerfile_path, build_path): diff --git a/build/model/config_model.py b/build/model/config_model.py index 9d93c7c8..bff5fc6d 100644 --- a/build/model/config_model.py +++ b/build/model/config_model.py @@ -44,6 +44,8 @@ def build_config_parse(self): if "docker-username" in buildConfigContent["cluster"]["docker-registry-info"] else None self.buildConfigDict["dockerRegistryInfo"]["dockerPassword"] = buildConfigContent["cluster"]["docker-registry-info"]["docker-password"] \ if "docker-password" in buildConfigContent["cluster"]["docker-registry-info"] else None + self.buildConfigDict["dockerRegistryInfo"]["managedIdentityId"] = buildConfigContent["cluster"]["docker-registry-info"]["managed-identity-id"] \ + if "managed-identity-id" in buildConfigContent["cluster"]["docker-registry-info"] else None self.buildConfigDict["dockerRegistryInfo"]["dockerTag"] = \ buildConfigContent["cluster"]["docker-registry-info"]["docker-tag"] self.buildConfigDict["dockerRegistryInfo"]["secretName"] = \ @@ -59,6 +61,8 @@ def build_config_parse(self): if "username" in buildConfigContent["cluster"]["docker-registry"] else None self.buildConfigDict["dockerRegistryInfo"]["dockerPassword"] = buildConfigContent["cluster"]["docker-registry"]["password"] \ if "password" in buildConfigContent["cluster"]["docker-registry"] else None + self.buildConfigDict["dockerRegistryInfo"]["managedIdentityId"] = buildConfigContent["cluster"]["docker-registry"]["managed-identity-id"] \ + if "managed-identity-id" in buildConfigContent["cluster"]["docker-registry"] else None self.buildConfigDict["dockerRegistryInfo"]["dockerTag"] = \ buildConfigContent["cluster"]["docker-registry"]["tag"] self.buildConfigDict["dockerRegistryInfo"]["secretName"] = \ diff --git a/build/pai_build.py b/build/pai_build.py index be4218eb..6970a370 100755 --- a/build/pai_build.py +++ b/build/pai_build.py @@ -105,6 +105,11 @@ def main(): default=None, help="The image list you want to build" ) + build_parser.add_argument( + '--managed-identity-id', + type=str, + help="The Azure managed identity ID for authentication" + ) build_parser.set_defaults(func=build_service) # Push commands @@ -147,6 +152,11 @@ def main(): type=str, help="The docker password you want to use for authentication, which will override the config file if '--docker-registry' is also set" ) + push_parser.add_argument( + '--managed-identity-id', + type=str, + help="The Azure managed identity ID for authentication, which will override the config file if '--docker-registry' is also set" + ) push_parser.add_argument( "--docker-tag", type=str, @@ -156,15 +166,18 @@ def main(): args = parser.parse_args() config_model = load_build_config(args.config) + if hasattr(args, 'docker_registry') and args.docker_registry is not None: config_model['dockerRegistryInfo']['dockerRegistryDomain'] = args.docker_registry - if args.docker_namespace is not None: + if hasattr(args, 'docker_namespace') and args.docker_namespace is not None: config_model['dockerRegistryInfo']['dockerNameSpace'] = args.docker_namespace - if args.docker_username is not None: + if hasattr(args, 'docker_username') and args.docker_username is not None: config_model['dockerRegistryInfo']['dockerUserName'] = args.docker_username - if args.docker_password is not None: + if hasattr(args, 'docker_password') and args.docker_password is not None: config_model['dockerRegistryInfo']['dockerPassword'] = args.docker_password - if args.docker_tag is not None: + if hasattr(args, 'managed_identity_id') and args.managed_identity_id is not None: + config_model['dockerRegistryInfo']['managedIdentityId'] = args.managed_identity_id + if hasattr(args, 'docker_tag') and args.docker_tag is not None: config_model['dockerRegistryInfo']['dockerTag'] = args.docker_tag args.func(args, config_model)