Skip to content
Draft
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
15 changes: 15 additions & 0 deletions api/v1alpha1/llamastackdistribution_types.go

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

29 changes: 29 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.

25 changes: 25 additions & 0 deletions config/crd/bases/llamastack.io_llamastackdistributions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,31 @@ spec:
x-kubernetes-validations:
- message: Only one of name or image can be specified
rule: '!(has(self.name) && has(self.image))'
envFromExternalConfigMaps:
description: EnvFromExternalConfigMaps defines external ConfigMaps
to inject as environment variables
items:
description: ExternalConfigMapSpec defines external ConfigMaps
to inject as environment variables
properties:
mapping:
additionalProperties:
type: string
description: |-
Mapping defines how ConfigMap keys map to environment variable names
Key is the ConfigMap key, Value is the environment variable name
type: object
name:
description: Name is the name of the ConfigMap
type: string
namespace:
description: Namespace is the namespace of the ConfigMap
type: string
required:
- name
- namespace
type: object
type: array
podOverrides:
description: PodOverrides allows advanced pod-level customization.
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apiVersion: llamastack.io/v1alpha1
kind: LlamaStackDistribution
metadata:
name: llamastackdistribution-with-external-configmaps
spec:
replicas: 1
server:
containerSpec:
env:
- name: INFERENCE_MODEL
value: 'llama3.2:1b'
- name: OLLAMA_URL
value: 'http://ollama-server-service.ollama-dist.svc.cluster.local:11434'
name: llama-stack
distribution:
name: starter
storage:
size: "20Gi"
mountPath: "/home/lls/.lls"
# External ConfigMaps configuration - enables referencing Konflux-built images
# from the redhat-ods-applications namespace ConfigMaps
envFromExternalConfigMaps:
- name: trustyai-service-operator-config
namespace: redhat-ods-applications
mapping:
ragas-provider-image: RAGAS_PROVIDER_IMAGE
garak-provider-image: GARAK_PROVIDER_IMAGE
---
# Example of how the ConfigMap would look like in redhat-ods-applications namespace
# This would be automatically created/managed by the trustyai-service-operator
apiVersion: v1
kind: ConfigMap
metadata:
name: trustyai-service-operator-config
namespace: redhat-ods-applications
data:
ragas-provider-image: "quay.io/trustyai/llama-stack-provider-ragas:v1.2.3-konflux-build-123"
garak-provider-image: "quay.io/trustyai/llama-stack-provider-trustyai-garak:v1.2.3-konflux-build-456"
# Other trustyai operator configuration values...
trustyaiServiceImage: "quay.io/trustyai/trustyai-service:latest"
trustyaiOperatorImage: "quay.io/trustyai/trustyai-service-operator:latest"
58 changes: 58 additions & 0 deletions controllers/resource_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (

llamav1alpha1 "github.com/llamastack/llama-stack-k8s-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand Down Expand Up @@ -186,6 +188,13 @@ func configureContainerEnvironment(ctx context.Context, r *LlamaStackDistributio
}
}

// Add environment variables from external ConfigMaps
if err := addExternalConfigMapEnvVars(ctx, r, instance, container); err != nil {
// Log error but don't fail deployment - allows graceful degradation
logger := log.FromContext(ctx)
logger.Error(err, "Failed to add external ConfigMap environment variables")
}

// Finally, add the user provided env vars
container.Env = append(container.Env, instance.Spec.Server.ContainerSpec.Env...)
}
Expand Down Expand Up @@ -662,3 +671,52 @@ func (r *LlamaStackDistributionReconciler) resolveImage(distribution llamav1alph
return "", errors.New("failed to validate distribution: either distribution.name or distribution.image must be set")
}
}

// addExternalConfigMapEnvVars adds environment variables from external ConfigMaps.
func addExternalConfigMapEnvVars(ctx context.Context, r *LlamaStackDistributionReconciler, instance *llamav1alpha1.LlamaStackDistribution, container *corev1.Container) error {
if len(instance.Spec.Server.EnvFromExternalConfigMaps) == 0 {
return nil
}

logger := log.FromContext(ctx)

for _, extConfigMap := range instance.Spec.Server.EnvFromExternalConfigMaps {
configMap := &corev1.ConfigMap{}
err := r.Get(ctx, types.NamespacedName{
Name: extConfigMap.Name,
Namespace: extConfigMap.Namespace,
}, configMap)

if err != nil {
if k8serrors.IsNotFound(err) {
logger.Info("External ConfigMap not found, skipping",
"configMapName", extConfigMap.Name,
"configMapNamespace", extConfigMap.Namespace)
continue
}
return fmt.Errorf("failed to get external ConfigMap %s/%s: %w",
extConfigMap.Namespace, extConfigMap.Name, err)
}

for configMapKey, envVarName := range extConfigMap.Mapping {
if value, exists := configMap.Data[configMapKey]; exists {
container.Env = append(container.Env, corev1.EnvVar{
Name: envVarName,
Value: value,
})
logger.V(1).Info("Added environment variable from external ConfigMap",
"configMapName", extConfigMap.Name,
"configMapNamespace", extConfigMap.Namespace,
"configMapKey", configMapKey,
"envVarName", envVarName)
} else {
logger.Info("ConfigMap key not found, skipping",
"configMapName", extConfigMap.Name,
"configMapNamespace", extConfigMap.Namespace,
"configMapKey", configMapKey)
}
}
}

return nil
}
146 changes: 146 additions & 0 deletions controllers/resource_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestBuildContainerSpec(t *testing.T) {
Expand Down Expand Up @@ -664,3 +667,146 @@ func newDefaultStartupProbe(port int32) *corev1.Probe {
SuccessThreshold: startupProbeSuccessThreshold,
}
}

func TestAddExternalConfigMapEnvVars(t *testing.T) {
testCases := []struct {
name string
instance *llamav1alpha1.LlamaStackDistribution
existingConfigMaps []corev1.ConfigMap
expectedEnvVars []corev1.EnvVar
expectError bool
}{
{
name: "no external configmaps configured",
instance: &llamav1alpha1.LlamaStackDistribution{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test-ns",
},
Spec: llamav1alpha1.LlamaStackDistributionSpec{
Server: llamav1alpha1.ServerSpec{
EnvFromExternalConfigMaps: nil,
},
},
},
existingConfigMaps: []corev1.ConfigMap{},
expectedEnvVars: []corev1.EnvVar{},
expectError: false,
},
{
name: "single external configmap with mapping",
instance: &llamav1alpha1.LlamaStackDistribution{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test-ns",
},
Spec: llamav1alpha1.LlamaStackDistributionSpec{
Server: llamav1alpha1.ServerSpec{
EnvFromExternalConfigMaps: []llamav1alpha1.ExternalConfigMapSpec{
{
Name: "trustyai-config",
Namespace: "redhat-ods-applications",
Mapping: map[string]string{
"ragas-provider-image": "RAGAS_PROVIDER_IMAGE",
"garak-provider-image": "GARAK_PROVIDER_IMAGE",
},
},
},
},
},
},
existingConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "trustyai-config",
Namespace: "redhat-ods-applications",
},
Data: map[string]string{
"ragas-provider-image": "quay.io/trustyai/ragas:v1.2.3-konflux-abc123",
"garak-provider-image": "quay.io/trustyai/garak:v1.2.3-konflux-def456",
"other-config": "should-be-ignored",
},
},
},
expectedEnvVars: []corev1.EnvVar{
{Name: "RAGAS_PROVIDER_IMAGE", Value: "quay.io/trustyai/ragas:v1.2.3-konflux-abc123"},
{Name: "GARAK_PROVIDER_IMAGE", Value: "quay.io/trustyai/garak:v1.2.3-konflux-def456"},
},
expectError: false,
},
{
name: "configmap not found - should not error but skip",
instance: &llamav1alpha1.LlamaStackDistribution{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test-ns",
},
Spec: llamav1alpha1.LlamaStackDistributionSpec{
Server: llamav1alpha1.ServerSpec{
EnvFromExternalConfigMaps: []llamav1alpha1.ExternalConfigMapSpec{
{
Name: "missing-config",
Namespace: "redhat-ods-applications",
Mapping: map[string]string{
"some-key": "SOME_ENV",
},
},
},
},
},
},
existingConfigMaps: []corev1.ConfigMap{},
expectedEnvVars: []corev1.EnvVar{},
expectError: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
require.NoError(t, llamav1alpha1.AddToScheme(scheme))

objs := make([]client.Object, len(tc.existingConfigMaps))
for i := range tc.existingConfigMaps {
objs[i] = &tc.existingConfigMaps[i]
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objs...).
Build()

reconciler := &LlamaStackDistributionReconciler{
Client: fakeClient,
}

container := &corev1.Container{
Name: "test-container",
Image: "test-image",
Env: []corev1.EnvVar{}, // Start with empty env vars
}

err := addExternalConfigMapEnvVars(t.Context(), reconciler, tc.instance, container)

if tc.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)

for _, expectedEnv := range tc.expectedEnvVars {
found := false
for _, actualEnv := range container.Env {
if actualEnv.Name == expectedEnv.Name && actualEnv.Value == expectedEnv.Value {
found = true
break
}
}
assert.True(t, found, "Expected env var %s=%s not found in container env vars", expectedEnv.Name, expectedEnv.Value)
}

assert.Len(t, container.Env, len(tc.expectedEnvVars), "Unexpected number of env vars added")
}
})
}
}
Loading