Repository: substratusai/kubeai (go)

Issue:
Feature Request: Support for Image Pull Secrets in Model CRD to Address Docker Hub Rate Limits
## Description
###  Background
Docker Hub has announced that starting April 1, 2025, unauthenticated users will be limited to only 10 pulls per hour.
Source: [Docker Hub Usage and Limits](https://docs.docker.com/docker-hub/usage/#:~:text=Starting%20April%201%2C%202025%2C%20all,Unauthenticated%20users%3A%2010%20pulls%2Fhour)

### Issue
Currently, the Model CRD does not support specifying image pull secrets directly. This limitation makes it difficult to use authenticated Docker Hub pulls or private images with KubeAI models.

### Feature Request
Add support for image pull secrets in the Model CRD to allow users to specify credentials for pulling container images.

### Proposed Solution
Add an imagePullSecrets field to the Model CRD spec, similar to how it works in Pod specs:
```yaml
apiVersion: kubeai.org/v1
kind: Model
metadata:
  name: my-model
spec:
  # ... existing fields ...
  imagePullSecrets:
  - name: my-docker-credentials
```

### Edit - Another Proposed Solution by @samos123:
Setting the image pull secrets array at the KubeAI config level 

### Benefits

- Helps users avoid Docker Hub rate limits by authenticating pulls
- Allows for using private images in model deployments
- Improves user experience in environments with multiple models

### Additional Context
This feature becomes increasingly important as Docker Hub enforces stricter rate limits. Without it, users may experience failed pulls and service disruptions when deploying models.


Pull request:
Add imagePullSecrets Support for Model Pods
# Add imagePullSecrets Support for Model Pods
## Description

This PR adds support for propagating imagePullSecrets from the main KubeAI configuration to model pods. This enables model pods to pull images from private registries using the same credentials as the main server.

## Changes

- Added `ImagePullSecrets` field to `ModelServerPods` configuration struct
- Updated chart values.yaml to document imagePullSecrets
-  Updated Helm chart configmap template to pass imagePullSecrets from values to system config
- Updated model engine reconcilers to pass ImagePullSecrets to pod specifications
- Added integration tests to verify ImagePullSecrets propagation

## Example Usage

In your Helm values.yaml:
```yaml
imagePullSecrets:
- name: my-registry-secret
- name: another-registry-secret
```

The secrets will automatically be used by all model pods created by KubeAI.

## Related Issues
Closes #433 

Patch:
diff --git a/charts/kubeai/templates/configmap.yaml b/charts/kubeai/templates/configmap.yaml
index 3f8f150..0350721 100644
--- a/charts/kubeai/templates/configmap.yaml
+++ b/charts/kubeai/templates/configmap.yaml
@@ -31,6 +31,10 @@ data:
       securityContext:
         {{- .Values.modelServerPods.securityContext | toYaml | nindent 8}}
       {{- end}}
+      {{- if .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml .Values.imagePullSecrets | nindent 8}}
+      {{- end}}
       {{- end}}
       serviceAccountName: {{ include "models.serviceAccountName" . }}
     modelAutoscaling:
diff --git a/charts/kubeai/values.yaml b/charts/kubeai/values.yaml
index 5104fcb..347a77d 100644
--- a/charts/kubeai/values.yaml
+++ b/charts/kubeai/values.yaml
@@ -252,6 +252,7 @@ image:
   # Overrides the image tag whose default is the chart appVersion.
   tag: ""
 
+# The imagePullSecrets will be used to pull both the kubeai controller image and the model images.
 imagePullSecrets: []
 nameOverride: ""
 fullnameOverride: "kubeai"
diff --git a/internal/config/system.go b/internal/config/system.go
index 2e6daaf..40909e2 100644
--- a/internal/config/system.go
+++ b/internal/config/system.go
@@ -242,4 +242,7 @@ type ModelServerPods struct {
 
 	// Security Context for the model pod containers
 	ModelContainerSecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
+
+	// ImagePullSecrets is a list of references to secrets in the same namespace to use for pulling any of the images
+	ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
 }
diff --git a/internal/modelcontroller/engine_fasterwhisper.go b/internal/modelcontroller/engine_fasterwhisper.go
index 30d7468..5bee45f 100644
--- a/internal/modelcontroller/engine_fasterwhisper.go
+++ b/internal/modelcontroller/engine_fasterwhisper.go
@@ -60,6 +60,7 @@ func (r *ModelReconciler) fasterWhisperPodForModel(m *kubeaiv1.Model, c ModelCon
 			RuntimeClassName:   c.RuntimeClassName,
 			ServiceAccountName: r.ModelServerPods.ModelServiceAccountName,
 			SecurityContext:    r.ModelServerPods.ModelPodSecurityContext,
+			ImagePullSecrets:   r.ModelServerPods.ImagePullSecrets,
 			Containers: []corev1.Container{
 				{
 					Name:            serverContainerName,
diff --git a/internal/modelcontroller/engine_infinity.go b/internal/modelcontroller/engine_infinity.go
index 1b69901..c93fa78 100644
--- a/internal/modelcontroller/engine_infinity.go
+++ b/internal/modelcontroller/engine_infinity.go
@@ -78,6 +78,7 @@ func (r *ModelReconciler) infinityPodForModel(m *kubeaiv1.Model, c ModelConfig)
 			RuntimeClassName:   c.RuntimeClassName,
 			ServiceAccountName: r.ModelServerPods.ModelServiceAccountName,
 			SecurityContext:    r.ModelServerPods.ModelPodSecurityContext,
+			ImagePullSecrets:   r.ModelServerPods.ImagePullSecrets,
 			Containers: []corev1.Container{
 				{
 					Name:  serverContainerName,
diff --git a/internal/modelcontroller/engine_ollama.go b/internal/modelcontroller/engine_ollama.go
index 3bd812e..db6a0b6 100644
--- a/internal/modelcontroller/engine_ollama.go
+++ b/internal/modelcontroller/engine_ollama.go
@@ -82,6 +82,7 @@ func (r *ModelReconciler) oLlamaPodForModel(m *kubeaiv1.Model, c ModelConfig) *c
 			RuntimeClassName:   c.RuntimeClassName,
 			ServiceAccountName: r.ModelServerPods.ModelServiceAccountName,
 			SecurityContext:    r.ModelServerPods.ModelPodSecurityContext,
+			ImagePullSecrets:   r.ModelServerPods.ImagePullSecrets,
 			Containers: []corev1.Container{
 				{
 					Name:            serverContainerName,
diff --git a/internal/modelcontroller/engine_vllm.go b/internal/modelcontroller/engine_vllm.go
index b13d48a..b61a17a 100644
--- a/internal/modelcontroller/engine_vllm.go
+++ b/internal/modelcontroller/engine_vllm.go
@@ -69,6 +69,7 @@ func (r *ModelReconciler) vLLMPodForModel(m *kubeaiv1.Model, c ModelConfig) *cor
 			RuntimeClassName:   c.RuntimeClassName,
 			ServiceAccountName: r.ModelServerPods.ModelServiceAccountName,
 			SecurityContext:    r.ModelServerPods.ModelPodSecurityContext,
+			ImagePullSecrets:   r.ModelServerPods.ImagePullSecrets,
 			Containers: []corev1.Container{
 				{
 					Name:            serverContainerName,


Test patch:
diff --git a/test/integration/main_test.go b/test/integration/main_test.go
index 4288739..f43c593 100644
--- a/test/integration/main_test.go
+++ b/test/integration/main_test.go
@@ -179,6 +179,19 @@ func baseSysCfg(t *testing.T) config.System {
 		ModelLoading: config.ModelLoading{
 			Image: "model-loader",
 		},
+		ModelServerPods: config.ModelServerPods{
+			ImagePullSecrets: []corev1.LocalObjectReference{
+				{Name: "test-secret1"},
+				{Name: "test-secret2"},
+			},
+			ModelServiceAccountName: "test-service-account",
+			ModelPodSecurityContext: &corev1.PodSecurityContext{
+				RunAsNonRoot: ptr.To(true),
+			},
+			ModelContainerSecurityContext: &corev1.SecurityContext{
+				AllowPrivilegeEscalation: ptr.To(false),
+			},
+		},
 		ResourceProfiles: map[string]config.ResourceProfile{
 			resourceProfileCPU: {
 				Requests: corev1.ResourceList{
diff --git a/test/integration/model_profiles_test.go b/test/integration/model_profiles_test.go
index 31b69a8..89adc14 100644
--- a/test/integration/model_profiles_test.go
+++ b/test/integration/model_profiles_test.go
@@ -52,7 +52,12 @@ func TestModelProfiles(t *testing.T) {
 		assert.Contains(t, pod.Spec.Tolerations, sysCfg.ResourceProfiles[resourceProfileCPU].Tolerations[0])
 		assert.Equal(t, sysCfg.ResourceProfiles[resourceProfileCPU].Affinity, pod.Spec.Affinity)
 		assert.Equal(t, sysCfg.ResourceProfiles[resourceProfileCPU].NodeSelector, pod.Spec.NodeSelector)
-	}, 5*time.Second, time.Second/10, "Resource profile should be applied to the model Pod object")
+		// Verify all ModelServerPods settings are correctly propagated
+		assert.Equal(t, sysCfg.ModelServerPods.ImagePullSecrets, pod.Spec.ImagePullSecrets)
+		assert.Equal(t, sysCfg.ModelServerPods.ModelServiceAccountName, pod.Spec.ServiceAccountName)
+		assert.Equal(t, sysCfg.ModelServerPods.ModelPodSecurityContext, pod.Spec.SecurityContext)
+		assert.Equal(t, sysCfg.ModelServerPods.ModelContainerSecurityContext, container.SecurityContext)
+	}, 5*time.Second, time.Second/10, "Resource profile and ModelServerPods settings should be applied to the model Pod object")
 
 	const userImage = "my-repo.com/my-repo/my-image:latest"
 	require.EventuallyWithT(t, func(t *assert.CollectT) {