diff --git a/stack_orchestrator/deploy/compose/deploy_docker.py b/stack_orchestrator/deploy/compose/deploy_docker.py index c6397aad..dca0ddfb 100644 --- a/stack_orchestrator/deploy/compose/deploy_docker.py +++ b/stack_orchestrator/deploy/compose/deploy_docker.py @@ -48,7 +48,7 @@ class DockerDeployer(Deployer): self.compose_project_name = compose_project_name self.compose_env_file = compose_env_file - def up(self, detach, skip_cluster_management, services): + def up(self, detach, skip_cluster_management, services, image_overrides=None): if not opts.o.dry_run: try: return self.docker.compose.up(detach=detach, services=services) diff --git a/stack_orchestrator/deploy/deploy.py b/stack_orchestrator/deploy/deploy.py index 30f88fa2..c8cf2535 100644 --- a/stack_orchestrator/deploy/deploy.py +++ b/stack_orchestrator/deploy/deploy.py @@ -137,7 +137,11 @@ def create_deploy_context( def up_operation( - ctx, services_list, stay_attached=False, skip_cluster_management=False + ctx, + services_list, + stay_attached=False, + skip_cluster_management=False, + image_overrides=None, ): global_context = ctx.parent.parent.obj deploy_context = ctx.obj @@ -156,6 +160,7 @@ def up_operation( detach=not stay_attached, skip_cluster_management=skip_cluster_management, services=services_list, + image_overrides=image_overrides, ) for post_start_command in cluster_context.post_start_commands: _run_command(global_context, cluster_context.cluster, post_start_command) diff --git a/stack_orchestrator/deploy/deployer.py b/stack_orchestrator/deploy/deployer.py index d8fb656b..6362cc48 100644 --- a/stack_orchestrator/deploy/deployer.py +++ b/stack_orchestrator/deploy/deployer.py @@ -20,7 +20,7 @@ from typing import Optional class Deployer(ABC): @abstractmethod - def up(self, detach, skip_cluster_management, services): + def up(self, detach, skip_cluster_management, services, image_overrides=None): pass @abstractmethod diff --git a/stack_orchestrator/deploy/deployment.py b/stack_orchestrator/deploy/deployment.py index 348cdbd0..0804b5a6 100644 --- a/stack_orchestrator/deploy/deployment.py +++ b/stack_orchestrator/deploy/deployment.py @@ -248,8 +248,13 @@ def run_job(ctx, job_name, helm_release): "--expected-ip", help="Expected IP for DNS verification (if different from egress)", ) +@click.option( + "--image", + multiple=True, + help="Override container image: container=image", +) @click.pass_context -def restart(ctx, stack_path, spec_file, config_file, force, expected_ip): +def restart(ctx, stack_path, spec_file, config_file, force, expected_ip, image): """Pull latest code and restart deployment using git-tracked spec. GitOps workflow: @@ -276,6 +281,17 @@ def restart(ctx, stack_path, spec_file, config_file, force, expected_ip): deployment_context: DeploymentContext = ctx.obj + # Parse --image flags into a dict of container_name -> image + image_overrides = {} + for entry in image: + if "=" not in entry: + raise click.BadParameter( + f"Invalid --image format '{entry}', expected container=image", + param_hint="'--image'", + ) + container_name, image_ref = entry.split("=", 1) + image_overrides[container_name] = image_ref + # Get current spec info (before git pull) current_spec = deployment_context.spec current_http_proxy = current_spec.get_http_proxy() @@ -389,7 +405,11 @@ def restart(ctx, stack_path, spec_file, config_file, force, expected_ip): print("\n[4/4] Applying deployment update...") ctx.obj = make_deploy_context(ctx) up_operation( - ctx, services_list=None, stay_attached=False, skip_cluster_management=True + ctx, + services_list=None, + stay_attached=False, + skip_cluster_management=True, + image_overrides=image_overrides or None, ) print("\n=== Restart Complete ===") diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index 6b5d6e60..f52aac06 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -115,6 +115,7 @@ class K8sDeployer(Deployer): ) -> None: self.type = type self.skip_cluster_management = False + self.image_overrides = None self.k8s_namespace = "default" # Will be overridden below if context exists # TODO: workaround pending refactoring above to cope with being # created with a null deployment_context @@ -412,6 +413,15 @@ class K8sDeployer(Deployer): return # Process compose files into a Deployment deployment = self.cluster_info.get_deployment(image_pull_policy="Always") + # Apply image overrides if provided + if self.image_overrides: + for container in deployment.spec.template.spec.containers: + if container.name in self.image_overrides: + container.image = self.image_overrides[container.name] + if opts.o.debug: + print( + f"Overriding image for {container.name}: {container.image}" + ) # Create or update the k8s Deployment if opts.o.debug: print(f"Sending this deployment: {deployment}") @@ -525,7 +535,8 @@ class K8sDeployer(Deployer): return cert return None - def up(self, detach, skip_cluster_management, services): + def up(self, detach, skip_cluster_management, services, image_overrides=None): + self.image_overrides = image_overrides self.skip_cluster_management = skip_cluster_management if not opts.o.dry_run: if self.is_kind() and not self.skip_cluster_management: