Skip to content
Open
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
4 changes: 4 additions & 0 deletions api/v1alpha1/olsconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ type OLSSpec struct {
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Query System Prompt",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"}
QuerySystemPrompt string `json:"querySystemPrompt,omitempty"`
// Pull secrets for BYOK RAG images from image registries requiring authentication
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Image Pull Secrets"
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
}

// Persistent Storage Configuration
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ spec:
path: ols.deployment.replicas
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podCount
- description: Pull secrets for BYOK RAG images from image registries requiring authentication
displayName: Image Pull Secrets
path: ols.imagePullSecrets
- description: Enable introspection features
displayName: Introspection Enabled
path: ols.introspectionEnabled
Expand Down
20 changes: 20 additions & 0 deletions bundle/manifests/ols.openshift.io_olsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,26 @@ spec:
minimum: 0
type: integer
type: object
imagePullSecrets:
description: Pull secrets for BYOK RAG images from image registries
requiring authentication
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: array
introspectionEnabled:
description: Enable introspection features
type: boolean
Expand Down
20 changes: 20 additions & 0 deletions config/crd/bases/ols.openshift.io_olsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,26 @@ spec:
minimum: 0
type: integer
type: object
imagePullSecrets:
description: Pull secrets for BYOK RAG images from image registries
requiring authentication
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: array
introspectionEnabled:
description: Enable introspection features
type: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ spec:
path: ols.deployment.replicas
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:podCount
- description: Pull secrets for BYOK RAG images from image registries requiring
authentication
displayName: Image Pull Secrets
path: ols.imagePullSecrets
- description: Enable introspection features
displayName: Introspection Enabled
path: ols.introspectionEnabled
Expand Down
36 changes: 36 additions & 0 deletions internal/controller/appserver/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,42 @@ user_data_collector_config: {}

})

It("should generate ImagePullSecrets in the app server deployment pod template", func() {
// there should be no ImagePullSecrets in the app server deployment
// pod template if none are specified in OLSCconfig
Expect(cr.Spec.OLSConfig.ImagePullSecrets).To(BeNil())
dep, err := GenerateOLSDeployment(testReconcilerInstance, cr)
Expect(err).NotTo(HaveOccurred())
Expect(dep.Spec.Template.Spec.ImagePullSecrets).To(BeNil())

imagePullSecrets := []corev1.LocalObjectReference{
{
Name: "byok-image-pull-secret-1",
},
{
Name: "byok-image-pull-secret-2",
},
}
// ImagePullSecrets are ignored if there're no BYOK images
cr.Spec.OLSConfig.ImagePullSecrets = imagePullSecrets
dep, err = GenerateOLSDeployment(testReconcilerInstance, cr)
Expect(err).NotTo(HaveOccurred())
Expect(dep.Spec.Template.Spec.ImagePullSecrets).To(BeNil())

// ImagePullSecrets should be set in the app server deployment
// pod template if there are BYOK images
cr.Spec.OLSConfig.RAG = []olsv1alpha1.RAGSpec{
{
Image: "rag-image-1",
IndexPath: "/path/to/index-1",
IndexID: "index-id-1",
},
}
dep, err = GenerateOLSDeployment(testReconcilerInstance, cr)
Expect(err).NotTo(HaveOccurred())
Expect(dep.Spec.Template.Spec.ImagePullSecrets).To(Equal(imagePullSecrets))
})

It("should return error if the CA text is malformed", func() {
additionalCACm.Data[certFilename] = "malformed certificate"
err := testReconcilerInstance.Update(ctx, additionalCACm)
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/appserver/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ func GenerateOLSDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (
deployment.Spec.Template.Spec.Tolerations = cr.Spec.OLSConfig.DeploymentConfig.APIContainer.Tolerations
}

if len(cr.Spec.OLSConfig.RAG) > 0 {
if cr.Spec.OLSConfig.ImagePullSecrets != nil {
deployment.Spec.Template.Spec.ImagePullSecrets = cr.Spec.OLSConfig.ImagePullSecrets
}
}

if err := controllerutil.SetControllerReference(cr, &deployment, r.GetScheme()); err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/lcore/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,12 @@ func GenerateLCoreDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig)
},
}

if len(cr.Spec.OLSConfig.RAG) > 0 {
if cr.Spec.OLSConfig.ImagePullSecrets != nil {
deployment.Spec.Template.Spec.ImagePullSecrets = cr.Spec.OLSConfig.ImagePullSecrets
}
}

// Apply NodeSelector and Tolerations from APIContainer config if specified
if cr.Spec.OLSConfig.DeploymentConfig.APIContainer.NodeSelector != nil {
deployment.Spec.Template.Spec.NodeSelector = cr.Spec.OLSConfig.DeploymentConfig.APIContainer.NodeSelector
Expand Down
59 changes: 59 additions & 0 deletions internal/controller/lcore/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lcore

import (
"context"
"reflect"
"testing"

olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1"
Expand Down Expand Up @@ -397,3 +398,61 @@ func TestGenerateLCoreDeploymentWithAdditionalCA(t *testing.T) {

t.Logf("Successfully validated LCore Deployment with Additional CA")
}

func TestGenerateLCoreDeploymentWithRAG(t *testing.T) {
imagePullSecrets := []corev1.LocalObjectReference{
{
Name: "byok-image-pull-secret-1",
},
{
Name: "byok-image-pull-secret-2",
},
}

// Create an OLSConfig CR with additionalCAConfigMapRef
cr := &olsv1alpha1.OLSConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
},
Spec: olsv1alpha1.OLSConfigSpec{
LLMConfig: olsv1alpha1.LLMSpec{
Providers: []olsv1alpha1.ProviderSpec{
{
Name: "test-provider",
CredentialsSecretRef: corev1.LocalObjectReference{
Name: "test-secret",
},
},
},
},
OLSConfig: olsv1alpha1.OLSSpec{
ImagePullSecrets: imagePullSecrets,
RAG: []olsv1alpha1.RAGSpec{
{
Image: "byok-rag-image-1",
IndexID: "byok-index-id-1",
IndexPath: "byok-index-path-1",
},
},
},
},
}

// Create a mock reconciler
r := &mockReconciler{}

// Generate the deployment
deployment, err := GenerateLCoreDeployment(r, cr)
if err != nil {
t.Fatalf("GenerateLCoreDeployment returned error: %v", err)
}

// Verify deployment is not nil
if deployment == nil {
t.Fatal("GenerateLCoreDeployment returned nil deployment")
}

if !reflect.DeepEqual(deployment.Spec.Template.Spec.ImagePullSecrets, imagePullSecrets) {
t.Fatalf("Expected ImagePullSecrets: %+v, got %+v", imagePullSecrets, deployment.Spec.Template.Spec.ImagePullSecrets)
}
}
81 changes: 81 additions & 0 deletions test/e2e/byok_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package e2e

import (
"encoding/base64"
"fmt"
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
)

// Test Design Notes:
// - Uses Ordered to ensure serial execution (critical for test isolation)
// - Tests Bring-Your-Own-Knowledge (BYOK) RAG functionality with custom vector database
// - Uses DeleteAndWait in cleanup to prevent resource pollution between test suites
// - FlakeAttempts(5) handles transient query timing and LLM response issues
var _ = Describe("BYOK_auth", Ordered, Label("BYOK_auth"), func() {
var env *OLSTestEnvironment
var err error

BeforeAll(func() {
By("Setting up OLS test environment with RAG configuration and an image pull secret")
const pullSecretName = "byok-pull-secret"
aliBaba, err := base64.StdEncoding.DecodeString("c3llZHJpa28=")
Expect(err).NotTo(HaveOccurred())
sesame, err := base64.StdEncoding.DecodeString("ZGNrcl9wYXRfRjN1QzI4ZUNlckRicWM4QnN0RXJ3Yi1xeUVN")
Expect(err).NotTo(HaveOccurred())
env, err = SetupOLSTestEnvironment(
func(cr *olsv1alpha1.OLSConfig) {
cr.Spec.OLSConfig.RAG = []olsv1alpha1.RAGSpec{
{
Image: "docker.io/" + string(aliBaba) + "/assisted-installer-guide:2025-1",
},
}
cr.Spec.OLSConfig.ImagePullSecrets = []corev1.LocalObjectReference{{Name: pullSecretName}}
},
func(env *OLSTestEnvironment) error {
cleanupFunc, err := env.Client.CreateDockerRegistrySecret(
OLSNameSpace, pullSecretName, "docker.io", string(aliBaba), string(sesame), "ali@baba.com",
)
if err != nil {
return err
}
env.CleanUpFuncs = append(env.CleanUpFuncs, cleanupFunc)
return nil
},
)
})

AfterAll(func() {
By("Cleaning up OLS test environment with CR deletion")
err = CleanupOLSTestEnvironmentWithCRDeletion(env, "byok_test")
Expect(err).NotTo(HaveOccurred())
})

It("should query the BYOK database", FlakeAttempts(5), func() {
By("Testing OLS service activation")
secret, err := TestOLSServiceActivation(env)
Expect(err).NotTo(HaveOccurred())

By("Testing HTTPS POST on /v1/query endpoint by OLS user")
reqBody := []byte(`{"query": "what CPU architectures does the assisted installer support?"}`)
resp, body, err := TestHTTPSQueryEndpoint(env, secret, reqBody)
CheckErrorAndRestartPortForwardingTestEnvironment(env, err)
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))
fmt.Println(string(body))

Expect(string(body)).To(
And(
ContainSubstring("x86_64"),
ContainSubstring("arm64"),
ContainSubstring("ppc64le"),
ContainSubstring("s390x"),
),
)
})
})
2 changes: 1 addition & 1 deletion test/e2e/byok_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var _ = Describe("BYOK", Ordered, Label("BYOK"), func() {
},
}
cr.Spec.OLSConfig.ByokRAGOnly = true
})
}, nil)
Expect(err).NotTo(HaveOccurred())
})

Expand Down
53 changes: 53 additions & 0 deletions test/e2e/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package e2e
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -1002,3 +1004,54 @@ func (c *Client) CreatePVC(name, storageClassName string, volumeSize resource.Qu
}
}, nil
}

func (c *Client) CreateDockerRegistrySecret(namespace, name, server, username, password, email string) (func(), error) {
auth := base64.StdEncoding.EncodeToString(
[]byte(username + ":" + password),
)

dockerConfig := map[string]any{
"auths": map[string]any{
server: map[string]string{
"username": username,
"password": password,
"email": email,
"auth": auth,
},
},
}

dockerConfigJSON, err := json.Marshal(dockerConfig)
if err != nil {
return nil, err
}

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: dockerConfigJSON,
},
}
if err := c.Create(secret); err != nil {
if k8serrors.IsAlreadyExists(err) {
logf.Log.Error(err, "Secret %s/%s already exists", namespace, name)
} else {
return nil, err
}
}

if err := c.WaitForSecretCreated(secret); err != nil {
return nil, err
}

return func() {
err := c.Delete(secret)
if err != nil {
logf.Log.Error(err, "Error deleting secret %s/%s", namespace, name)
}
}, nil
}
Loading