diff --git a/api/v1alpha1/pipeline_types.go b/api/v1alpha1/pipeline_types.go index b8ea150..014f990 100644 --- a/api/v1alpha1/pipeline_types.go +++ b/api/v1alpha1/pipeline_types.go @@ -184,6 +184,13 @@ type Filter struct { // +optional Env []corev1.EnvVar `json:"env,omitempty"` + // envFrom is a list of sources to populate environment variables in the container. + // Uses the standard Kubernetes EnvFromSource type for full compatibility + // Note: values defined in env (including those injected by the controller) + // take precedence over values from envFrom, regardless of ordering. + // +optional + EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"` + // args are the command arguments to pass to the container // +optional Args []string `json:"args,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index fc0b10c..c29883f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -121,6 +121,13 @@ func (in *Filter) DeepCopyInto(out *Filter) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]corev1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Args != nil { in, out := &in.Args, &out.Args *out = make([]string, len(*in)) diff --git a/config/crd/bases/filter.plainsight.ai_pipelines.yaml b/config/crd/bases/filter.plainsight.ai_pipelines.yaml index 8357a30..17a6daf 100644 --- a/config/crd/bases/filter.plainsight.ai_pipelines.yaml +++ b/config/crd/bases/filter.plainsight.ai_pipelines.yaml @@ -242,6 +242,58 @@ spec: - name type: object type: array + envFrom: + description: |- + envFrom is a list of sources to populate environment variables in the container. + Uses the standard Kubernetes EnvFromSource type for full compatibility + Note: values defined in env (including those injected by the controller) + take precedence over values from envFrom, regardless of ordering. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array image: description: image is the container image to run (e.g., "myregistry/filter:v1.0") type: string diff --git a/deployment/openfilter-pipelines-controller/crds/filter.plainsight.ai_pipelines.yaml b/deployment/openfilter-pipelines-controller/crds/filter.plainsight.ai_pipelines.yaml index 8357a30..17a6daf 100644 --- a/deployment/openfilter-pipelines-controller/crds/filter.plainsight.ai_pipelines.yaml +++ b/deployment/openfilter-pipelines-controller/crds/filter.plainsight.ai_pipelines.yaml @@ -242,6 +242,58 @@ spec: - name type: object type: array + envFrom: + description: |- + envFrom is a list of sources to populate environment variables in the container. + Uses the standard Kubernetes EnvFromSource type for full compatibility + Note: values defined in env (including those injected by the controller) + take precedence over values from envFrom, regardless of ordering. + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array image: description: image is the container image to run (e.g., "myregistry/filter:v1.0") type: string diff --git a/internal/controller/pipelineinstance_controller_batch.go b/internal/controller/pipelineinstance_controller_batch.go index b09860d..e529027 100644 --- a/internal/controller/pipelineinstance_controller_batch.go +++ b/internal/controller/pipelineinstance_controller_batch.go @@ -563,6 +563,7 @@ func (r *PipelineInstanceReconciler) buildJob(ctx context.Context, pipelineInsta Command: filter.Command, Args: filter.Args, Env: containerEnv, + EnvFrom: filter.EnvFrom, ImagePullPolicy: filter.ImagePullPolicy, VolumeMounts: []corev1.VolumeMount{ {Name: "workspace", MountPath: "/ws"}, diff --git a/internal/controller/pipelineinstance_controller_batch_test.go b/internal/controller/pipelineinstance_controller_batch_test.go index 651623f..a9bfe3d 100644 --- a/internal/controller/pipelineinstance_controller_batch_test.go +++ b/internal/controller/pipelineinstance_controller_batch_test.go @@ -1161,3 +1161,40 @@ func TestBuildJob_StreamKeyUsesNamespacePrefix(t *testing.T) { } t.Error("expected STREAM env var in claimer, not found") } + +func TestBuildJob_EnvFrom_Propagated(t *testing.T) { + r := makeMinimalReconciler() + pi := makeMinimalPipelineInstance() + ps := makeMinimalPipelineSource() + + pipeline := &pipelinesv1alpha1.Pipeline{ + Spec: pipelinesv1alpha1.PipelineSpec{ + Filters: []pipelinesv1alpha1.Filter{ + { + Name: "f-with-envfrom", + Image: "image:latest", + EnvFrom: []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}}}, + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}}, + }, + }, + }, + }, + } + + job := r.buildJob(context.Background(), pi, pipeline, ps, "test-job") + containers := job.Spec.Template.Spec.Containers + if len(containers) == 0 { + t.Fatal("expected at least one container") + } + ef := containers[0].EnvFrom + if len(ef) != 2 { + t.Fatalf("expected 2 EnvFrom entries, got %d: %v", len(ef), ef) + } + if ef[0].ConfigMapRef == nil || ef[0].ConfigMapRef.Name != "my-config" { + t.Errorf("expected first EnvFrom to be ConfigMapRef my-config, got %v", ef[0].ConfigMapRef) + } + if ef[1].SecretRef == nil || ef[1].SecretRef.Name != "my-secret" { + t.Errorf("expected second EnvFrom to be SecretRef my-secret, got %v", ef[1].SecretRef) + } +} diff --git a/internal/controller/pipelineinstance_controller_streaming.go b/internal/controller/pipelineinstance_controller_streaming.go index df74ac0..c2d4486 100644 --- a/internal/controller/pipelineinstance_controller_streaming.go +++ b/internal/controller/pipelineinstance_controller_streaming.go @@ -391,6 +391,7 @@ func (r *PipelineInstanceReconciler) buildStreamingDeployment(ctx context.Contex envVars = append(envVars, filter.Env...) container.Env = envVars + container.EnvFrom = filter.EnvFrom if filter.Resources != nil { container.Resources = *filter.Resources diff --git a/internal/controller/pipelineinstance_controller_streaming_test.go b/internal/controller/pipelineinstance_controller_streaming_test.go index 856c549..0935025 100644 --- a/internal/controller/pipelineinstance_controller_streaming_test.go +++ b/internal/controller/pipelineinstance_controller_streaming_test.go @@ -923,3 +923,40 @@ func TestBuildRTSPURLWithCredentials(t *testing.T) { }) } } + +func TestBuildStreamingDeployment_EnvFrom_Propagated(t *testing.T) { + r := makeMinimalReconciler() + pi := makeMinimalPipelineInstance() + ps := makeMinimalPipelineSource() + + pipeline := &pipelinesv1alpha1.Pipeline{ + Spec: pipelinesv1alpha1.PipelineSpec{ + Filters: []pipelinesv1alpha1.Filter{ + { + Name: "f-with-envfrom", + Image: "image:latest", + EnvFrom: []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}}}, + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}}}, + }, + }, + }, + }, + } + + dep := r.buildStreamingDeployment(context.Background(), pi, pipeline, ps, "test-deploy") + containers := dep.Spec.Template.Spec.Containers + if len(containers) == 0 { + t.Fatal("expected at least one container") + } + ef := containers[0].EnvFrom + if len(ef) != 2 { + t.Fatalf("expected 2 EnvFrom entries, got %d: %v", len(ef), ef) + } + if ef[0].ConfigMapRef == nil || ef[0].ConfigMapRef.Name != "my-config" { + t.Errorf("expected first EnvFrom to be ConfigMapRef my-config, got %v", ef[0].ConfigMapRef) + } + if ef[1].SecretRef == nil || ef[1].SecretRef.Name != "my-secret" { + t.Errorf("expected second EnvFrom to be SecretRef my-secret, got %v", ef[1].SecretRef) + } +}