From bb4ed5e9b10b21e1e70499842b844cb261a709da Mon Sep 17 00:00:00 2001 From: Scot Schuchert-Wells Date: Wed, 11 Nov 2026 14:57:38 +0000 Subject: [PATCH] fix: keep pull secrets and sysctls on flag deploy A flag deploy rewrites the whole workload spec and built a sandbox with only the flag-described container. Image pull secrets and sysctls set by a manifest were silently dropped, so the next instance of a workload pulling a private image stuck at ImageUnavailable. Key changes: - carry an existing workload's imagePullSecrets and sysctls forward when the flag path rebuilds the sandbox - say so in the deploy help next to the other carried-forward settings - add a unit test for the create, redeploy and no-sandbox cases Fixes #432 --- internal/cmd/compute/deploy/deploy.go | 24 ++++++++--- internal/cmd/compute/deploy/sandbox_test.go | 46 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 internal/cmd/compute/deploy/sandbox_test.go diff --git a/internal/cmd/compute/deploy/deploy.go b/internal/cmd/compute/deploy/deploy.go index 3e2dea5d..1734b6e8 100644 --- a/internal/cmd/compute/deploy/deploy.go +++ b/internal/cmd/compute/deploy/deploy.go @@ -163,7 +163,8 @@ template, so setting or removing one replaces the workload's instances. All of these carry forward. A deploy that does not mention a variable, an import, a mount, or a label keeps the one the workload already has, so a -routine image bump changes nothing else. Remove one explicitly with a trailing +routine image bump changes nothing else. Image pull secrets and sysctls set +by a manifest carry forward too. Remove one explicitly with a trailing "-": --env KEY-, --env-from-secret NAME-, --configmap NAME-, --label key-.`, Args: cobra.MaximumNArgs(1), Example: ` # Deploy with flags @@ -599,10 +600,8 @@ func deployFromFlags(cmd *cobra.Command, workloadName string, opts *options) err Resources: computev1alpha.InstanceRuntimeResources{ InstanceType: inputs.instanceType, }, - Sandbox: &computev1alpha.SandboxRuntime{ - Containers: []computev1alpha.SandboxContainer{container}, - }, - Class: runtimeClass, + Sandbox: sandboxRuntime(&workload, creating, container), + Class: runtimeClass, }, NetworkInterfaces: networkInterfaces, Volumes: config.volumes, @@ -1043,6 +1042,21 @@ func resolveRuntimeClass(existing *computev1alpha.Workload, creating bool, reque existing.Name, current, requested, existing.Name) } +// sandboxRuntime returns the sandbox a flag-driven deploy writes: the one +// container the flags describe, plus the existing workload's image pull +// secrets and sysctls. No flag sets those, so rewriting the sandbox without +// them would silently drop them and leave a private image unpullable. +func sandboxRuntime(existing *computev1alpha.Workload, creating bool, container computev1alpha.SandboxContainer) *computev1alpha.SandboxRuntime { + sandbox := &computev1alpha.SandboxRuntime{ + Containers: []computev1alpha.SandboxContainer{container}, + } + if current := existing.Spec.Template.Spec.Runtime.Sandbox; !creating && current != nil { + sandbox.ImagePullSecrets = current.ImagePullSecrets + sandbox.Sysctls = current.Sysctls + } + return sandbox +} + // resolveNetworkInterfaces returns the network interfaces a flag-driven deploy // writes. A new workload gets one interface on the requested network, or on // "default" when none is requested. diff --git a/internal/cmd/compute/deploy/sandbox_test.go b/internal/cmd/compute/deploy/sandbox_test.go new file mode 100644 index 00000000..e9acc39b --- /dev/null +++ b/internal/cmd/compute/deploy/sandbox_test.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package deploy + +import ( + "reflect" + "testing" + + computev1alpha "go.datum.net/compute/api/v1alpha" +) + +func TestSandboxRuntime(t *testing.T) { + secrets := []computev1alpha.LocalSecretReference{{Name: "registry-creds"}} + sysctls := []computev1alpha.SandboxSysctl{{Name: "net.core.somaxconn", Value: "1024"}} + container := computev1alpha.SandboxContainer{Name: containerName, Image: "ghcr.io/acme/api:2"} + + stored := workloadWithPorts() + stored.Spec.Template.Spec.Runtime.Sandbox.ImagePullSecrets = secrets + stored.Spec.Template.Spec.Runtime.Sandbox.Sysctls = sysctls + + tests := []struct { + name string + existing *computev1alpha.Workload + creating bool + wantSecrets []computev1alpha.LocalSecretReference + wantSysctls []computev1alpha.SandboxSysctl + }{ + {name: "redeploy keeps pull secrets and sysctls", existing: &stored, wantSecrets: secrets, wantSysctls: sysctls}, + {name: "create starts empty", existing: workload(), creating: true}, + {name: "redeploy of a workload without a sandbox", existing: workload()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sandboxRuntime(tt.existing, tt.creating, container) + if !reflect.DeepEqual(got.Containers, []computev1alpha.SandboxContainer{container}) { + t.Errorf("containers = %+v, want only the flag-built container", got.Containers) + } + if !reflect.DeepEqual(got.ImagePullSecrets, tt.wantSecrets) { + t.Errorf("imagePullSecrets = %+v, want %+v", got.ImagePullSecrets, tt.wantSecrets) + } + if !reflect.DeepEqual(got.Sysctls, tt.wantSysctls) { + t.Errorf("sysctls = %+v, want %+v", got.Sysctls, tt.wantSysctls) + } + }) + } +}