Skip to content

Commit ea1046f

Browse files
Merge pull request #232 from vimalk78/ols-3274-sandbox-audit-env
OLS-3274: inject audit env vars into sandbox pods from AgenticOLSConfig
2 parents 041c9f2 + 1155906 commit ea1046f

7 files changed

Lines changed: 183 additions & 6 deletions

File tree

controller/proposal/bare_pod_manager.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ func (m *BarePodManager) Claim(ctx context.Context, proposalName, step, _ string
6767
return "", fmt.Errorf("%s: %w", ErrBuildPodSpec, err)
6868
}
6969

70+
if err := appendAuditEnvVars(ctx, m.Client, &podSpec.Containers[0]); err != nil {
71+
return "", fmt.Errorf("append audit env vars: %w", err)
72+
}
73+
7074
pod := &corev1.Pod{
7175
ObjectMeta: metav1.ObjectMeta{
7276
Name: podName,

controller/proposal/bare_pod_manager_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
func newBarePodClient() *fake.ClientBuilder {
1818
s := runtime.NewScheme()
1919
utilruntime.Must(corev1.AddToScheme(s))
20+
utilruntime.Must(agenticv1alpha1.AddToScheme(s))
2021
return fake.NewClientBuilder().WithScheme(s)
2122
}
2223

@@ -131,6 +132,101 @@ func TestBarePodManager_Release_NotFound(t *testing.T) {
131132
}
132133
}
133134

135+
func TestBarePodManager_Claim_AuditEnabled_DefaultsTrue(t *testing.T) {
136+
fc := newBarePodClient().Build()
137+
builder := &PodSpecBuilder{Image: "quay.io/test/sandbox:latest"}
138+
m := NewBarePodManager(fc, builder, "test-ns")
139+
m.SetStep(
140+
&agenticv1alpha1.Agent{Spec: agenticv1alpha1.AgentSpec{Model: "claude-opus-4-6"}},
141+
testLLMProvider(agenticv1alpha1.LLMProviderAnthropic),
142+
nil,
143+
defaultSandboxSA,
144+
)
145+
146+
name, err := m.Claim(context.Background(), "my-proposal", "analysis", "")
147+
if err != nil {
148+
t.Fatalf("Claim: %v", err)
149+
}
150+
151+
var pod corev1.Pod
152+
if err := fc.Get(context.Background(), types.NamespacedName{Name: name, Namespace: "test-ns"}, &pod); err != nil {
153+
t.Fatalf("pod not created: %v", err)
154+
}
155+
env := envToMap(pod.Spec.Containers[0].Env)
156+
if env["LIGHTSPEED_AUDIT_ENABLED"] != "true" {
157+
t.Errorf("LIGHTSPEED_AUDIT_ENABLED = %q, want true", env["LIGHTSPEED_AUDIT_ENABLED"])
158+
}
159+
if _, ok := env["OTEL_EXPORTER_OTLP_ENDPOINT"]; ok {
160+
t.Error("OTEL_EXPORTER_OTLP_ENDPOINT should not be set when no config CR exists")
161+
}
162+
}
163+
164+
func TestBarePodManager_Claim_AuditWithOTELEndpoint(t *testing.T) {
165+
config := &agenticv1alpha1.AgenticOLSConfig{}
166+
config.Name = "cluster"
167+
config.Spec.Audit = agenticv1alpha1.AuditConfig{
168+
Logging: agenticv1alpha1.AuditLoggingEnabled,
169+
OTEL: agenticv1alpha1.AuditOTELConfig{Endpoint: "jaeger:4317"},
170+
}
171+
fc := newBarePodClient().WithObjects(config).Build()
172+
builder := &PodSpecBuilder{Image: "quay.io/test/sandbox:latest"}
173+
m := NewBarePodManager(fc, builder, "test-ns")
174+
m.SetStep(
175+
&agenticv1alpha1.Agent{Spec: agenticv1alpha1.AgentSpec{Model: "claude-opus-4-6"}},
176+
testLLMProvider(agenticv1alpha1.LLMProviderAnthropic),
177+
nil,
178+
defaultSandboxSA,
179+
)
180+
181+
name, err := m.Claim(context.Background(), "my-proposal", "analysis", "")
182+
if err != nil {
183+
t.Fatalf("Claim: %v", err)
184+
}
185+
186+
var pod corev1.Pod
187+
if err := fc.Get(context.Background(), types.NamespacedName{Name: name, Namespace: "test-ns"}, &pod); err != nil {
188+
t.Fatalf("pod not created: %v", err)
189+
}
190+
env := envToMap(pod.Spec.Containers[0].Env)
191+
if env["LIGHTSPEED_AUDIT_ENABLED"] != "true" {
192+
t.Errorf("LIGHTSPEED_AUDIT_ENABLED = %q, want true", env["LIGHTSPEED_AUDIT_ENABLED"])
193+
}
194+
if env["OTEL_EXPORTER_OTLP_ENDPOINT"] != "jaeger:4317" {
195+
t.Errorf("OTEL_EXPORTER_OTLP_ENDPOINT = %q, want jaeger:4317", env["OTEL_EXPORTER_OTLP_ENDPOINT"])
196+
}
197+
}
198+
199+
func TestBarePodManager_Claim_AuditDisabled(t *testing.T) {
200+
config := &agenticv1alpha1.AgenticOLSConfig{}
201+
config.Name = "cluster"
202+
config.Spec.Audit = agenticv1alpha1.AuditConfig{
203+
Logging: agenticv1alpha1.AuditLoggingDisabled,
204+
}
205+
fc := newBarePodClient().WithObjects(config).Build()
206+
builder := &PodSpecBuilder{Image: "quay.io/test/sandbox:latest"}
207+
m := NewBarePodManager(fc, builder, "test-ns")
208+
m.SetStep(
209+
&agenticv1alpha1.Agent{Spec: agenticv1alpha1.AgentSpec{Model: "claude-opus-4-6"}},
210+
testLLMProvider(agenticv1alpha1.LLMProviderAnthropic),
211+
nil,
212+
defaultSandboxSA,
213+
)
214+
215+
name, err := m.Claim(context.Background(), "my-proposal", "analysis", "")
216+
if err != nil {
217+
t.Fatalf("Claim: %v", err)
218+
}
219+
220+
var pod corev1.Pod
221+
if err := fc.Get(context.Background(), types.NamespacedName{Name: name, Namespace: "test-ns"}, &pod); err != nil {
222+
t.Fatalf("pod not created: %v", err)
223+
}
224+
env := envToMap(pod.Spec.Containers[0].Env)
225+
if _, ok := env["LIGHTSPEED_AUDIT_ENABLED"]; ok {
226+
t.Error("LIGHTSPEED_AUDIT_ENABLED should not be set when audit logging is disabled")
227+
}
228+
}
229+
134230
func TestBarePodManager_WaitReady_ImmediateReady(t *testing.T) {
135231
pod := &corev1.Pod{}
136232
pod.Name = "ls-analysis-my-proposal"

controller/proposal/helpers.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ func isSuspended(ctx context.Context, c client.Client) (bool, error) {
7272
return config.Spec.Suspended, nil
7373
}
7474

75+
func readAuditConfig(ctx context.Context, c client.Client) (*agenticv1alpha1.AuditConfig, error) {
76+
var config agenticv1alpha1.AgenticOLSConfig
77+
if err := c.Get(ctx, client.ObjectKey{Name: "cluster"}, &config); err != nil {
78+
if client.IgnoreNotFound(err) == nil {
79+
return nil, nil
80+
}
81+
return nil, err
82+
}
83+
return &config.Spec.Audit, nil
84+
}
85+
7586
// failStep marks a step as failed and creates a failure result CR.
7687
// The caller must have set the step condition to ConditionUnknown before
7788
// calling failStep so that conditionTime can extract the start time.

controller/proposal/podspec_builder.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package proposal
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"path"
@@ -9,6 +10,7 @@ import (
910
corev1 "k8s.io/api/core/v1"
1011
"k8s.io/apimachinery/pkg/util/intstr"
1112
"k8s.io/utils/ptr"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
1214

1315
agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1"
1416
)
@@ -129,6 +131,20 @@ func (b *PodSpecBuilder) Build(
129131
}, nil
130132
}
131133

134+
func appendAuditEnvVars(ctx context.Context, c client.Client, container *corev1.Container) error {
135+
audit, err := readAuditConfig(ctx, c)
136+
if err != nil {
137+
return fmt.Errorf("read audit config: %w", err)
138+
}
139+
if audit.LoggingEnabled() {
140+
container.Env = append(container.Env, corev1.EnvVar{Name: "LIGHTSPEED_AUDIT_ENABLED", Value: "true"})
141+
}
142+
if endpoint := audit.OTELEndpoint(); endpoint != "" {
143+
container.Env = append(container.Env, corev1.EnvVar{Name: "OTEL_EXPORTER_OTLP_ENDPOINT", Value: endpoint})
144+
}
145+
return nil
146+
}
147+
132148
func (b *PodSpecBuilder) addProviderSpecificEnv(container *corev1.Container, llm *agenticv1alpha1.LLMProvider) {
133149
switch llm.Spec.Type {
134150
case agenticv1alpha1.LLMProviderAnthropic:

controller/proposal/sandbox_templates.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ type templateHashInput struct {
9595
Step string `json:"step"`
9696
BaseResourceVersion string `json:"baseRV"`
9797
ServiceAccount string `json:"serviceAccount"`
98+
AuditLogging bool `json:"auditLogging"`
99+
OTELEndpoint string `json:"otelEndpoint,omitempty"`
98100
}
99101

100102
func computeTemplateHash(
@@ -106,6 +108,7 @@ func computeTemplateHash(
106108
step string,
107109
baseResourceVersion string,
108110
serviceAccount string,
111+
audit *agenticv1alpha1.AuditConfig,
109112
) (string, error) {
110113
input := templateHashInput{
111114
LLM: llm.Spec,
@@ -116,6 +119,8 @@ func computeTemplateHash(
116119
Step: step,
117120
BaseResourceVersion: baseResourceVersion,
118121
ServiceAccount: serviceAccount,
122+
AuditLogging: audit.LoggingEnabled(),
123+
OTELEndpoint: audit.OTELEndpoint(),
119124
}
120125
data, err := json.Marshal(input)
121126
if err != nil {
@@ -168,7 +173,11 @@ func EnsureAgentTemplate(
168173
requiredSecrets = tools.RequiredSecrets
169174
}
170175

171-
hash, err := computeTemplateHash(llm, agent.Spec.Model, skills, mcpServers, requiredSecrets, step, base.GetResourceVersion(), serviceAccount)
176+
audit, err := readAuditConfig(ctx, c)
177+
if err != nil {
178+
return "", fmt.Errorf("read audit config: %w", err)
179+
}
180+
hash, err := computeTemplateHash(llm, agent.Spec.Model, skills, mcpServers, requiredSecrets, step, base.GetResourceVersion(), serviceAccount, audit)
172181
if err != nil {
173182
return "", fmt.Errorf("%s: %w", ErrComputeTemplateHash, err)
174183
}
@@ -222,6 +231,10 @@ func EnsureAgentTemplate(
222231
return "", fmt.Errorf("%s: %w", ErrPatchLLMCredentials, err)
223232
}
224233

234+
if err := patchAuditEnvVars(derived, audit); err != nil {
235+
return "", fmt.Errorf("patch audit env vars: %w", err)
236+
}
237+
225238
if len(mcpServers) > 0 {
226239
if err := patchMCPServers(derived, mcpServers); err != nil {
227240
return "", fmt.Errorf("%s: %w", ErrPatchMCPServers, err)
@@ -735,6 +748,20 @@ type mcpHeaderEnvEntry struct {
735748
SecretName string `json:"secretName,omitempty"`
736749
}
737750

751+
func patchAuditEnvVars(tmpl *unstructured.Unstructured, audit *agenticv1alpha1.AuditConfig) error {
752+
if audit.LoggingEnabled() {
753+
if err := setEnvVar(tmpl, "LIGHTSPEED_AUDIT_ENABLED", "true"); err != nil {
754+
return fmt.Errorf("set LIGHTSPEED_AUDIT_ENABLED: %w", err)
755+
}
756+
}
757+
if endpoint := audit.OTELEndpoint(); endpoint != "" {
758+
if err := setEnvVar(tmpl, "OTEL_EXPORTER_OTLP_ENDPOINT", endpoint); err != nil {
759+
return fmt.Errorf("set OTEL_EXPORTER_OTLP_ENDPOINT: %w", err)
760+
}
761+
}
762+
return nil
763+
}
764+
738765
func patchMCPServers(tmpl *unstructured.Unstructured, servers []agenticv1alpha1.MCPServerConfig) error {
739766
entries := make([]mcpServerEnvEntry, 0, len(servers))
740767
for _, s := range servers {

controller/proposal/sandbox_templates_test.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func emptyTemplate() *unstructured.Unstructured {
7777

7878
func mustHash(t *testing.T, llm *agenticv1alpha1.LLMProvider, model string, skills []agenticv1alpha1.SkillsSource, requiredSecrets []agenticv1alpha1.SecretRequirement, phase string) string {
7979
t.Helper()
80-
h, err := computeTemplateHash(llm, model, skills, nil, requiredSecrets, phase, "", "")
80+
h, err := computeTemplateHash(llm, model, skills, nil, requiredSecrets, phase, "", "", nil)
8181
if err != nil {
8282
t.Fatalf("computeTemplateHash: %v", err)
8383
}
@@ -627,11 +627,11 @@ func TestComputeTemplateHash_DifferentBaseResourceVersion(t *testing.T) {
627627
llm := testLLMProvider(agenticv1alpha1.LLMProviderGoogleCloudVertex)
628628
skills := []agenticv1alpha1.SkillsSource{{Image: "quay.io/test/skills:latest"}}
629629

630-
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "")
630+
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "", nil)
631631
if err != nil {
632632
t.Fatal(err)
633633
}
634-
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "2000", "")
634+
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "2000", "", nil)
635635
if err != nil {
636636
t.Fatal(err)
637637
}
@@ -645,11 +645,11 @@ func TestComputeTemplateHash_SameBaseResourceVersion(t *testing.T) {
645645
llm := testLLMProvider(agenticv1alpha1.LLMProviderGoogleCloudVertex)
646646
skills := []agenticv1alpha1.SkillsSource{{Image: "quay.io/test/skills:latest"}}
647647

648-
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "")
648+
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "", nil)
649649
if err != nil {
650650
t.Fatal(err)
651651
}
652-
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "")
652+
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "", nil)
653653
if err != nil {
654654
t.Fatal(err)
655655
}
@@ -659,6 +659,27 @@ func TestComputeTemplateHash_SameBaseResourceVersion(t *testing.T) {
659659
}
660660
}
661661

662+
func TestComputeTemplateHash_DifferentAuditConfig(t *testing.T) {
663+
llm := testLLMProvider(agenticv1alpha1.LLMProviderAnthropic)
664+
skills := []agenticv1alpha1.SkillsSource{{Image: "quay.io/test/skills:latest"}}
665+
666+
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "", nil)
667+
if err != nil {
668+
t.Fatal(err)
669+
}
670+
audit := &agenticv1alpha1.AuditConfig{
671+
OTEL: agenticv1alpha1.AuditOTELConfig{Endpoint: "jaeger:4317"},
672+
}
673+
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "", audit)
674+
if err != nil {
675+
t.Fatal(err)
676+
}
677+
678+
if h1 == h2 {
679+
t.Error("different audit config should produce different hashes")
680+
}
681+
}
682+
662683
func TestPatchProbes(t *testing.T) {
663684
t.Run("sets readiness and liveness probes on first container", func(t *testing.T) {
664685
tmpl := emptyTemplate()

controller/proposal/sandbox_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"k8s.io/apimachinery/pkg/runtime"
1111
"k8s.io/apimachinery/pkg/runtime/schema"
1212
"k8s.io/apimachinery/pkg/types"
13+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1314
"sigs.k8s.io/controller-runtime/pkg/client"
1415
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1516

@@ -18,6 +19,7 @@ import (
1819

1920
func newSandboxClient(objects ...client.Object) client.Client {
2021
s := runtime.NewScheme()
22+
utilruntime.Must(agenticv1alpha1.AddToScheme(s))
2123

2224
mapper := apimeta.NewDefaultRESTMapper([]schema.GroupVersion{
2325
{Group: "extensions.agents.x-k8s.io", Version: "v1alpha1"},

0 commit comments

Comments
 (0)