Skip to content

Commit 9e9acd6

Browse files
authored
adding a default docker image to the k8 backend (#42)
1 parent b9246ce commit 9e9acd6

9 files changed

Lines changed: 152 additions & 11 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ backend:
6161
kubernetes:
6262
kubeconfig: "/path/to/kubeconfig"
6363
namespace: "agents"
64+
default_image: "my-registry.io/dev-image:latest"
6465
unschedulable_timeout: "2m"
6566
pod_template:
6667
nodeSelector:
@@ -75,6 +76,7 @@ backend:
7576

7677
Notes:
7778

79+
- `default_image` sets the Docker image for task Jobs when no Warp environment is configured on the run; this lets you skip creating a Warp environment entirely if all your tasks use the same base image (precedence: Warp environment image > `default_image` > `ubuntu:22.04`)
7880
- `namespace` selects the namespace inside the chosen cluster; it does not choose the cluster itself, and defaults to `default` when omitted
7981
- `unschedulable_timeout` controls how long a Pod may remain unschedulable before the task is failed early; it defaults to `30s`, and `0s` disables that fail-fast behavior
8082
- `image_pull_policy` defaults to `IfNotPresent`

charts/oz-agent-worker/templates/configmap.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ data:
1515
backend:
1616
kubernetes:
1717
namespace: {{ default .Release.Namespace .Values.kubernetesBackend.namespace | quote }}
18+
{{- if .Values.kubernetesBackend.defaultImage }}
19+
default_image: {{ .Values.kubernetesBackend.defaultImage | quote }}
20+
{{- end }}
1821
image_pull_policy: {{ .Values.kubernetesBackend.imagePullPolicy | quote }}
1922
unschedulable_timeout: {{ .Values.kubernetesBackend.unschedulableTimeout | quote }}
2023
{{- if .Values.kubernetesBackend.preflightImage }}

charts/oz-agent-worker/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ worker:
6666

6767
kubernetesBackend:
6868
namespace: ""
69+
defaultImage: ""
6970
imagePullPolicy: IfNotPresent
7071
preflightImage: ""
7172
setupCommand: ""

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type DirectConfig struct {
5656
type KubernetesConfig struct {
5757
Namespace string `yaml:"namespace"`
5858
Kubeconfig string `yaml:"kubeconfig"`
59+
DefaultImage string `yaml:"default_image" validate:"omitempty,no_whitespace"`
5960
ImagePullPolicy string `yaml:"image_pull_policy" validate:"omitempty,oneof=Always Never IfNotPresent"`
6061
PreflightImage string `yaml:"preflight_image" validate:"omitempty,no_whitespace"`
6162
SetupCommand string `yaml:"setup_command"`

internal/config/config_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,56 @@ backend:
436436
}
437437
}
438438

439+
func TestLoadKubernetesDefaultImage(t *testing.T) {
440+
t.Run("parses default_image when set", func(t *testing.T) {
441+
path := writeTestConfig(t, `
442+
worker_id: "k8s-worker"
443+
backend:
444+
kubernetes:
445+
default_image: "my-registry.io/custom-image:latest"
446+
`)
447+
cfg, err := Load(path)
448+
if err != nil {
449+
t.Fatalf("unexpected error: %v", err)
450+
}
451+
if cfg.Backend.Kubernetes == nil {
452+
t.Fatal("expected kubernetes backend to be set")
453+
}
454+
if cfg.Backend.Kubernetes.DefaultImage != "my-registry.io/custom-image:latest" {
455+
t.Errorf("default_image = %q, want %q", cfg.Backend.Kubernetes.DefaultImage, "my-registry.io/custom-image:latest")
456+
}
457+
})
458+
459+
t.Run("default_image is empty when not set", func(t *testing.T) {
460+
path := writeTestConfig(t, `
461+
worker_id: "k8s-worker"
462+
backend:
463+
kubernetes:
464+
namespace: "agents"
465+
`)
466+
cfg, err := Load(path)
467+
if err != nil {
468+
t.Fatalf("unexpected error: %v", err)
469+
}
470+
if cfg.Backend.Kubernetes.DefaultImage != "" {
471+
t.Errorf("expected default_image to be empty, got %q", cfg.Backend.Kubernetes.DefaultImage)
472+
}
473+
})
474+
475+
t.Run("rejects default_image with whitespace", func(t *testing.T) {
476+
path := writeTestConfig(t, `
477+
worker_id: "k8s-worker"
478+
backend:
479+
kubernetes:
480+
default_image: "my image:latest"
481+
`)
482+
_, err := Load(path)
483+
if err == nil {
484+
t.Fatal("expected error for default_image with whitespace")
485+
}
486+
})
487+
}
488+
439489
func TestLoadLegacyKubernetesFieldRejected(t *testing.T) {
440490
tests := []string{
441491
"image_pull_secret",

internal/worker/kubernetes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type KubernetesBackendConfig struct {
4949
WorkerID string
5050
Namespace string
5151
Kubeconfig string
52+
DefaultImage string
5253
ImagePullPolicy string
5354
PreflightImage string
5455
SetupCommand string

internal/worker/worker.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -347,17 +347,9 @@ func (w *Worker) handleTaskAssignment(assignment *types.TaskAssignmentMessage) {
347347
func (w *Worker) prepareTaskParams(assignment *types.TaskAssignmentMessage) *TaskParams {
348348
task := assignment.Task
349349

350-
// Resolve Docker image with default fallback.
351-
dockerImage := assignment.DockerImage
352-
if dockerImage == "" {
353-
dockerImage = "ubuntu:22.04"
354-
if task.AgentConfigSnapshot != nil && task.AgentConfigSnapshot.EnvironmentID != nil {
355-
log.Warnf(w.ctx, "Environment %s specified but no Docker image resolved. Using default: %s",
356-
*task.AgentConfigSnapshot.EnvironmentID, dockerImage)
357-
} else {
358-
log.Infof(w.ctx, "No environment specified, using default image: %s", dockerImage)
359-
}
360-
}
350+
// Resolve Docker image.
351+
// Precedence: server-provided image (from environment) > worker config default_image > hardcoded ubuntu:22.04.
352+
dockerImage := w.defaultImageForTask(assignment.DockerImage, task)
361353

362354
// Build common environment variables.
363355
envVars := []string{
@@ -406,6 +398,26 @@ func (w *Worker) prepareTaskParams(assignment *types.TaskAssignmentMessage) *Tas
406398
}
407399
}
408400

401+
// defaultImageForTask returns the Docker image to use for a task, applying the
402+
// precedence: server-provided > worker config default_image > hardcoded fallback.
403+
func (w *Worker) defaultImageForTask(assignmentImage string, task *types.Task) string {
404+
if assignmentImage != "" {
405+
return assignmentImage
406+
}
407+
if w.config.Kubernetes != nil && w.config.Kubernetes.DefaultImage != "" {
408+
log.Infof(w.ctx, "Using worker-configured default image: %s", w.config.Kubernetes.DefaultImage)
409+
return w.config.Kubernetes.DefaultImage
410+
}
411+
fallback := "ubuntu:22.04"
412+
if task.AgentConfigSnapshot != nil && task.AgentConfigSnapshot.EnvironmentID != nil {
413+
log.Warnf(w.ctx, "Environment %s specified but no Docker image resolved. Using default: %s",
414+
*task.AgentConfigSnapshot.EnvironmentID, fallback)
415+
} else {
416+
log.Infof(w.ctx, "No environment specified, using default image: %s", fallback)
417+
}
418+
return fallback
419+
}
420+
409421
func (w *Worker) executeTask(ctx context.Context, assignment *types.TaskAssignmentMessage) {
410422
defer func() {
411423
w.tasksMutex.Lock()

internal/worker/worker_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package worker
33
import (
44
"context"
55
"testing"
6+
7+
"github.com/warpdotdev/oz-agent-worker/internal/types"
68
)
79

810
type shutdownRecordingBackend struct {
@@ -19,6 +21,72 @@ func (b *shutdownRecordingBackend) Shutdown(ctx context.Context) {
1921
b.shutdownCtxErr = ctx.Err()
2022
}
2123

24+
func TestDefaultImageForTask(t *testing.T) {
25+
newWorker := func(defaultImage string) *Worker {
26+
ctx := context.Background()
27+
var k8sConfig *KubernetesBackendConfig
28+
if defaultImage != "" {
29+
k8sConfig = &KubernetesBackendConfig{DefaultImage: defaultImage}
30+
}
31+
return &Worker{
32+
ctx: ctx,
33+
config: Config{
34+
Kubernetes: k8sConfig,
35+
},
36+
}
37+
}
38+
39+
envID := "env-123"
40+
41+
t.Run("server-provided image wins over default_image", func(t *testing.T) {
42+
w := newWorker("my-registry.io/default:v1")
43+
got := w.defaultImageForTask("server-image:latest", &types.Task{})
44+
if got != "server-image:latest" {
45+
t.Errorf("got %q, want %q", got, "server-image:latest")
46+
}
47+
})
48+
49+
t.Run("default_image used when server image empty", func(t *testing.T) {
50+
w := newWorker("my-registry.io/default:v1")
51+
got := w.defaultImageForTask("", &types.Task{})
52+
if got != "my-registry.io/default:v1" {
53+
t.Errorf("got %q, want %q", got, "my-registry.io/default:v1")
54+
}
55+
})
56+
57+
t.Run("hardcoded fallback when no default_image configured", func(t *testing.T) {
58+
w := newWorker("")
59+
got := w.defaultImageForTask("", &types.Task{})
60+
if got != "ubuntu:22.04" {
61+
t.Errorf("got %q, want %q", got, "ubuntu:22.04")
62+
}
63+
})
64+
65+
t.Run("hardcoded fallback when kubernetes config nil", func(t *testing.T) {
66+
w := &Worker{
67+
ctx: context.Background(),
68+
config: Config{},
69+
}
70+
got := w.defaultImageForTask("", &types.Task{})
71+
if got != "ubuntu:22.04" {
72+
t.Errorf("got %q, want %q", got, "ubuntu:22.04")
73+
}
74+
})
75+
76+
t.Run("hardcoded fallback with environment ID logs warning", func(t *testing.T) {
77+
w := newWorker("")
78+
task := &types.Task{
79+
AgentConfigSnapshot: &types.AmbientAgentConfig{
80+
EnvironmentID: &envID,
81+
},
82+
}
83+
got := w.defaultImageForTask("", task)
84+
if got != "ubuntu:22.04" {
85+
t.Errorf("got %q, want %q", got, "ubuntu:22.04")
86+
}
87+
})
88+
}
89+
2290
func TestWorkerShutdownUsesFreshContextForBackendCleanup(t *testing.T) {
2391
workerCtx, cancel := context.WithCancel(context.Background())
2492
backend := &shutdownRecordingBackend{}

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
154154
var (
155155
namespace string
156156
kubeconfig string
157+
defaultImage string
157158
imagePullPolicy string
158159
preflightImage string
159160
setupCmd string
@@ -170,6 +171,7 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
170171
kc := fileConfig.Backend.Kubernetes
171172
namespace = kc.Namespace
172173
kubeconfig = kc.Kubeconfig
174+
defaultImage = kc.DefaultImage
173175
imagePullPolicy = kc.ImagePullPolicy
174176
preflightImage = kc.PreflightImage
175177
setupCmd = kc.SetupCommand
@@ -208,6 +210,7 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
208210
WorkerID: workerID,
209211
Namespace: namespace,
210212
Kubeconfig: kubeconfig,
213+
DefaultImage: defaultImage,
211214
ImagePullPolicy: imagePullPolicy,
212215
PreflightImage: preflightImage,
213216
SetupCommand: setupCmd,

0 commit comments

Comments
 (0)