From 6ace024cd30aa3b2962a50e635ec58eba8a87ad2 Mon Sep 17 00:00:00 2001 From: "A. F. Dudley" Date: Thu, 19 Mar 2026 03:44:57 +0000 Subject: [PATCH] fix: use replace instead of patch for k8s resource updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strategic merge patch preserves fields not present in the patch body. This means removed volumes, ports, and env vars persist in the running Deployment after a restart. Replace sends the complete spec built from the current compose files — removed fields are actually deleted. Affects Deployment, Service, Ingress, and NodePort updates. Service replace preserves clusterIP (immutable field) by reading it from the existing resource before replacing. Co-Authored-By: Claude Opus 4.6 (1M context) --- stack_orchestrator/deploy/k8s/deploy_k8s.py | 41 ++++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index f52aac06..787f20fd 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -437,10 +437,18 @@ class K8sDeployer(Deployer): print(f"Created Deployment {name}") except ApiException as e: if e.status == 409: - # Already exists — patch to trigger rolling update + # Already exists — replace to ensure removed fields + # (volumes, mounts, env vars) are actually deleted. + # Patch uses strategic merge which preserves old fields. + existing = self.apps_api.read_namespaced_deployment( + name=name, namespace=self.k8s_namespace + ) + deployment.metadata.resource_version = ( + existing.metadata.resource_version + ) deployment_resp = cast( client.V1Deployment, - self.apps_api.patch_namespaced_deployment( + self.apps_api.replace_namespaced_deployment( name=name, namespace=self.k8s_namespace, body=deployment, @@ -469,8 +477,16 @@ class K8sDeployer(Deployer): print(f"Created Service {svc_name}") except ApiException as e: if e.status == 409: - # Service exists — patch it (preserves clusterIP) - service_resp = self.core_api.patch_namespaced_service( + # Replace to ensure removed ports are deleted. + # Must preserve clusterIP (immutable) and resourceVersion. + existing = self.core_api.read_namespaced_service( + name=svc_name, namespace=self.k8s_namespace + ) + service.metadata.resource_version = ( + existing.metadata.resource_version + ) + service.spec.cluster_ip = existing.spec.cluster_ip + service_resp = self.core_api.replace_namespaced_service( name=svc_name, namespace=self.k8s_namespace, body=service, @@ -624,7 +640,13 @@ class K8sDeployer(Deployer): print(f"Created Ingress {ing_name}") except ApiException as e: if e.status == 409: - self.networking_api.patch_namespaced_ingress( + existing = self.networking_api.read_namespaced_ingress( + name=ing_name, namespace=self.k8s_namespace + ) + ingress.metadata.resource_version = ( + existing.metadata.resource_version + ) + self.networking_api.replace_namespaced_ingress( name=ing_name, namespace=self.k8s_namespace, body=ingress, @@ -648,7 +670,14 @@ class K8sDeployer(Deployer): ) except ApiException as e: if e.status == 409: - self.core_api.patch_namespaced_service( + existing = self.core_api.read_namespaced_service( + name=np_name, namespace=self.k8s_namespace + ) + nodeport.metadata.resource_version = ( + existing.metadata.resource_version + ) + nodeport.spec.cluster_ip = existing.spec.cluster_ip + self.core_api.replace_namespaced_service( name=np_name, namespace=self.k8s_namespace, body=nodeport,