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
5 changes: 5 additions & 0 deletions api/lmes/v1alpha1/lmevaljob_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,11 @@ type OCISpec struct {
// Path within the results to package as artifact
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._/-]*$`
Path string `json:"path"`
// Subject for the OCI artifact
// +optional
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9._:/@-]*$`
// +kubebuilder:validation:MaxLength=255
Subject string `json:"subject,omitempty"`
// Username for registry authentication
// +optional
UsernameRef *corev1.SecretKeySelector `json:"username,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ spec:
- key
type: object
x-kubernetes-map-type: atomic
subject:
description: Subject for the OCI artifact
pattern: ^[a-zA-Z0-9._:/@-]*$
type: string
tag:
description: Optional tag for the artifact (defaults to job
name if not specified)
Expand Down
10 changes: 9 additions & 1 deletion controllers/lmes/lmevaljob_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ func CreatePod(svcOpts *serviceOptions, job *lmesv1alpha1.LMEvalJob, log logr.Lo
}

// Always add OCI env vars if configured, regardless of offline/online mode
if job.Spec.HasOCIOutput() {
if job.Spec.HasOCIOutput() && job.Spec.Outputs != nil && job.Spec.Outputs.OCISpec != nil {
ociEnvVars := []corev1.EnvVar{
{
Name: "OCI_REGISTRY",
Expand Down Expand Up @@ -1162,6 +1162,14 @@ func CreatePod(svcOpts *serviceOptions, job *lmesv1alpha1.LMEvalJob, log logr.Lo
})
}

// Add subject if specified
if job.Spec.Outputs.OCISpec.Subject != "" {
ociEnvVars = append(ociEnvVars, corev1.EnvVar{
Name: "OCI_SUBJECT",
Value: job.Spec.Outputs.OCISpec.Subject,
})
}

// Handle authentication - either username/password or token
if job.Spec.Outputs.OCISpec.HasUsernamePassword() {
ociAuthEnvVars := []corev1.EnvVar{
Expand Down
232 changes: 232 additions & 0 deletions controllers/lmes/lmevaljob_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4008,6 +4008,238 @@ func Test_OCIPodConfiguration(t *testing.T) {
assert.Equal(t, "custom-tag-v1.0", envMap["OCI_TAG"].Value)
})

t.Run("OCIWithSubject", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Name: "test-oci-subject",
Namespace: "default",
},
Spec: lmesv1alpha1.LMEvalJobSpec{
Model: "test",
TaskList: lmesv1alpha1.TaskList{
TaskNames: []string{"task1"},
},
Outputs: &lmesv1alpha1.Outputs{
OCISpec: &lmesv1alpha1.OCISpec{
Registry: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "registry",
},
Repository: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "repository",
},
Subject: "llama-2-7b-chat",
Path: "results",
TokenRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-token"},
Key: "token",
},
},
},
},
}

pod := CreatePod(svcOpts, job, logger)
assert.NotNil(t, pod, "Should generate pod successfully")

// Check environment variables
envVars := pod.Spec.Containers[0].Env
envMap := make(map[string]corev1.EnvVar)
for _, env := range envVars {
envMap[env.Name] = env
}

// Verify subject is set
assert.Contains(t, envMap, "OCI_SUBJECT", "Should have OCI_SUBJECT env var")
assert.Equal(t, "llama-2-7b-chat", envMap["OCI_SUBJECT"].Value)
})

t.Run("OCIWithSubjectOmitted", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Name: "test-oci-subject-omitted",
Namespace: "default",
},
Spec: lmesv1alpha1.LMEvalJobSpec{
Model: "test",
TaskList: lmesv1alpha1.TaskList{
TaskNames: []string{"task1"},
},
Outputs: &lmesv1alpha1.Outputs{
OCISpec: &lmesv1alpha1.OCISpec{
Registry: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "registry",
},
Repository: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "repository",
},
Path: "results",
TokenRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-token"},
Key: "token",
},
},
},
},
}

pod := CreatePod(svcOpts, job, logger)
assert.NotNil(t, pod, "Should generate pod successfully")

// Check environment variables
envVars := pod.Spec.Containers[0].Env
envMap := make(map[string]corev1.EnvVar)
for _, env := range envVars {
envMap[env.Name] = env
}

// Verify OCI_SUBJECT is not set when omitted
assert.NotContains(t, envMap, "OCI_SUBJECT", "Should not have OCI_SUBJECT env var when omitted")
})

t.Run("OCIWithSubjectEmpty", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Name: "test-oci-subject-empty",
Namespace: "default",
},
Spec: lmesv1alpha1.LMEvalJobSpec{
Model: "test",
TaskList: lmesv1alpha1.TaskList{
TaskNames: []string{"task1"},
},
Outputs: &lmesv1alpha1.Outputs{
OCISpec: &lmesv1alpha1.OCISpec{
Registry: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "registry",
},
Repository: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "repository",
},
Subject: "",
Path: "results",
TokenRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-token"},
Key: "token",
},
},
},
},
}

pod := CreatePod(svcOpts, job, logger)
assert.NotNil(t, pod, "Should generate pod successfully")

// Check environment variables
envVars := pod.Spec.Containers[0].Env
envMap := make(map[string]corev1.EnvVar)
for _, env := range envVars {
envMap[env.Name] = env
}

// Verify OCI_SUBJECT is not set when empty
assert.NotContains(t, envMap, "OCI_SUBJECT", "Should not have OCI_SUBJECT env var when empty")
})

t.Run("OCIWithSubjectSpecialChars", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Name: "test-oci-subject-special",
Namespace: "default",
},
Spec: lmesv1alpha1.LMEvalJobSpec{
Model: "test",
TaskList: lmesv1alpha1.TaskList{
TaskNames: []string{"task1"},
},
Outputs: &lmesv1alpha1.Outputs{
OCISpec: &lmesv1alpha1.OCISpec{
Registry: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "registry",
},
Repository: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "repository",
},
Subject: "valid-subject-123",
Path: "results",
TokenRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-token"},
Key: "token",
},
},
},
},
}

pod := CreatePod(svcOpts, job, logger)
assert.NotNil(t, pod, "Should generate pod successfully")

// Check environment variables
envVars := pod.Spec.Containers[0].Env
envMap := make(map[string]corev1.EnvVar)
for _, env := range envVars {
envMap[env.Name] = env
}

// Verify subject with valid special characters is set
assert.Contains(t, envMap, "OCI_SUBJECT", "Should have OCI_SUBJECT env var")
assert.Equal(t, "valid-subject-123", envMap["OCI_SUBJECT"].Value)
})

t.Run("OCIWithSubjectDigestFormat", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Name: "test-oci-subject-digest",
Namespace: "default",
},
Spec: lmesv1alpha1.LMEvalJobSpec{
Model: "test",
TaskList: lmesv1alpha1.TaskList{
TaskNames: []string{"task1"},
},
Outputs: &lmesv1alpha1.Outputs{
OCISpec: &lmesv1alpha1.OCISpec{
Registry: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "registry",
},
Repository: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-secret"},
Key: "repository",
},
Subject: "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678",
Path: "results",
TokenRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "oci-token"},
Key: "token",
},
},
},
},
}

pod := CreatePod(svcOpts, job, logger)
assert.NotNil(t, pod, "Should generate pod successfully")

// Check environment variables
envVars := pod.Spec.Containers[0].Env
envMap := make(map[string]corev1.EnvVar)
for _, env := range envVars {
envMap[env.Name] = env
}

// Verify subject with OCI digest format is set
assert.Contains(t, envMap, "OCI_SUBJECT", "Should have OCI_SUBJECT env var")
assert.Equal(t, "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678", envMap["OCI_SUBJECT"].Value)
})

t.Run("OCIWithCertificates", func(t *testing.T) {
job := &lmesv1alpha1.LMEvalJob{
ObjectMeta: v1.ObjectMeta{
Expand Down
Loading