Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/internal/services/instance_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ func usesWebtopImage(instanceType string) bool {
}
}

// defaultImagePullPolicy returns the image pull policy to use for instance
// pods. Operators can override the default ("IfNotPresent") by setting the
// IMAGE_PULL_POLICY environment variable to "Always", "Never", or
// "IfNotPresent".
func defaultImagePullPolicy() string {
if v := strings.TrimSpace(os.Getenv("IMAGE_PULL_POLICY")); v != "" {
return v
}
return "IfNotPresent"
}

func defaultEgressProxyURL() (string, bool) {
if override := strings.TrimSpace(os.Getenv("CLAWMANAGER_EGRESS_PROXY_URL")); override != "" {
return override, true
Expand Down
42 changes: 42 additions & 0 deletions backend/internal/services/instance_runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package services

import "testing"

func TestDefaultImagePullPolicy_Default(t *testing.T) {
// With no env var set, should return "IfNotPresent".
t.Setenv("IMAGE_PULL_POLICY", "")
got := defaultImagePullPolicy()
if got != "IfNotPresent" {
t.Fatalf("expected IfNotPresent, got %q", got)
}
}

func TestDefaultImagePullPolicy_EnvOverride(t *testing.T) {
cases := []struct {
env string
want string
}{
{"Always", "Always"},
{"Never", "Never"},
{"IfNotPresent", "IfNotPresent"},
}
for _, tc := range cases {
t.Run(tc.env, func(t *testing.T) {
t.Setenv("IMAGE_PULL_POLICY", tc.env)
got := defaultImagePullPolicy()
if got != tc.want {
t.Fatalf("expected %q, got %q", tc.want, got)
}
})
}
}

func TestDefaultImagePullPolicy_WhitespaceOnly(t *testing.T) {
// Whitespace-only env var should fall back to default.
t.Setenv("IMAGE_PULL_POLICY", " ")
got := defaultImagePullPolicy()
if got != "IfNotPresent" {
t.Fatalf("expected IfNotPresent, got %q", got)
}
}

3 changes: 3 additions & 0 deletions backend/internal/services/instance_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"clawreef/internal/repository"
"clawreef/internal/services/k8s"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -313,6 +314,7 @@ func (s *instanceService) Create(userID int, req CreateInstanceRequest) (*models
Image: runtimeConfig.Image,
MountPath: runtimeConfig.MountPath,
ContainerPort: runtimeConfig.Port,
ImagePullPolicy: corev1.PullPolicy(defaultImagePullPolicy()),
ExtraEnv: extraEnv,
EnvFromSecretNames: []string{bootstrapSecretName},
}
Expand Down Expand Up @@ -488,6 +490,7 @@ func (s *instanceService) Start(instanceID int) error {
Image: runtimeConfig.Image,
MountPath: instance.MountPath,
ContainerPort: runtimeConfig.Port,
ImagePullPolicy: corev1.PullPolicy(defaultImagePullPolicy()),
ExtraEnv: extraEnv,
EnvFromSecretNames: []string{bootstrapSecretName},
}
Expand Down
14 changes: 12 additions & 2 deletions backend/internal/services/k8s/pod_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type PodConfig struct {
Image string
MountPath string
ContainerPort int32
ImagePullPolicy corev1.PullPolicy
ExtraEnv map[string]string
EnvFromSecretNames []string
}
Expand Down Expand Up @@ -84,6 +85,14 @@ func (s *PodService) CreatePod(ctx context.Context, config PodConfig) (*corev1.P
config.ContainerPort = 3001
}

// Default image pull policy to IfNotPresent so that air-gapped and
// enterprise environments can use locally cached images without being
// forced to pull from a remote registry (fixes #94).
pullPolicy := config.ImagePullPolicy
if pullPolicy == "" {
pullPolicy = corev1.PullIfNotPresent
}

pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Expand All @@ -101,8 +110,9 @@ func (s *PodService) CreatePod(ctx context.Context, config PodConfig) (*corev1.P
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "desktop",
Image: config.Image,
Name: "desktop",
Image: config.Image,
ImagePullPolicy: pullPolicy,
Ports: []corev1.ContainerPort{
{
ContainerPort: config.ContainerPort,
Expand Down
Loading