Skip to content

Commit fd26024

Browse files
committed
feat: add dataSource PVC mount and IntelliAide proposal templates
1 parent cae48d8 commit fd26024

7 files changed

Lines changed: 314 additions & 22 deletions

File tree

api/v1alpha1/proposal_types.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,22 @@ func (s ProposalStep) IsZero() bool {
270270
return s.Agent == "" && s.Tools.IsZero()
271271
}
272272

273+
// DataSource references a pre-existing PersistentVolumeClaim containing
274+
// input data for this proposal (e.g., must-gather bundles, diagnostic data).
275+
// The PVC must already exist in the same namespace as the Proposal and be
276+
// pre-populated with data before the Proposal is created. The operator
277+
// mounts it read-only at a well-known path (/data/input) accessible to
278+
// all skills in the sandbox pod.
279+
type DataSource struct {
280+
// claimName is the name of the PersistentVolumeClaim to mount.
281+
// The PVC must exist in the same namespace as the Proposal.
282+
// +required
283+
// +kubebuilder:validation:MinLength=1
284+
// +kubebuilder:validation:MaxLength=253
285+
// +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="must be a valid DNS subdomain"
286+
ClaimName string `json:"claimName"`
287+
}
288+
273289
// ProposalSpec defines the desired state of Proposal.
274290
//
275291
// A Proposal defines the workflow shape inline, specifying which steps
@@ -284,6 +300,7 @@ func (s ProposalStep) IsZero() bool {
284300
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.analysis) || (has(self.analysis) && self.analysis == oldSelf.analysis)",message="analysis is immutable once set"
285301
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.execution) || (has(self.execution) && self.execution == oldSelf.execution)",message="execution is immutable once set"
286302
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.verification) || (has(self.verification) && self.verification == oldSelf.verification)",message="verification is immutable once set"
303+
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataSource) || (has(self.dataSource) && self.dataSource == oldSelf.dataSource)",message="dataSource is immutable once set"
287304
type ProposalSpec struct {
288305
// request is the user's original request, alert description, or a
289306
// description of what triggered this proposal. This text is passed to
@@ -341,6 +358,15 @@ type ProposalSpec struct {
341358
// +optional
342359
Tools ToolsSpec `json:"tools,omitzero"`
343360

361+
// dataSource references a PVC containing pre-populated input data
362+
// (e.g., must-gather bundles, diagnostic data). The operator mounts
363+
// it read-only at /data/input in the sandbox pod. Skills discover
364+
// input data at this standard location.
365+
//
366+
// Immutable: input data source is fixed at creation.
367+
// +optional
368+
DataSource *DataSource `json:"dataSource,omitzero"`
369+
344370
// analysis defines per-step configuration for the analysis step,
345371
// including which agent handles it and any per-step tools.
346372
//
@@ -362,6 +388,18 @@ type ProposalSpec struct {
362388
// +optional
363389
Verification ProposalStep `json:"verification,omitzero"`
364390

391+
// timeoutMinutes sets the per-step timeout for sandbox agent calls.
392+
// This controls how long the operator waits for the sandbox pod to
393+
// become ready and for the agent to complete its work. Increase this
394+
// for long-running tools (e.g., IntelliAide RCA takes 10-30 minutes).
395+
// Defaults to 5 minutes when omitted.
396+
//
397+
// Mutable: can be adjusted before approving a step.
398+
// +optional
399+
// +kubebuilder:validation:Minimum=1
400+
// +kubebuilder:validation:Maximum=60
401+
TimeoutMinutes *int32 `json:"timeoutMinutes,omitempty"`
402+
365403
// revisionFeedback is the user's free-text feedback requesting changes
366404
// to the analysis. Patching this field bumps metadata.generation, which
367405
// the operator detects (generation > observedGeneration) and triggers
@@ -374,15 +412,6 @@ type ProposalSpec struct {
374412
// +kubebuilder:validation:MaxLength=32768
375413
RevisionFeedback string `json:"revisionFeedback,omitempty"`
376414

377-
// timeoutMinutes overrides the default sandbox operation timeout for
378-
// this proposal. When set, all sandbox wait and HTTP client timeouts
379-
// use this value instead of the operator default (5 minutes).
380-
//
381-
// Immutable: timeout policy is fixed at creation.
382-
// +optional
383-
// +kubebuilder:validation:Minimum=1
384-
// +kubebuilder:validation:Maximum=120
385-
TimeoutMinutes *int32 `json:"timeoutMinutes,omitzero"`
386415
}
387416

388417
// ProposalStatus defines the observed state of Proposal. All fields are

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

controller/proposal/sandbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (m *SandboxManager) buildClaim(claimName, proposalName, step, templateName
9090
func (m *SandboxManager) Claim(ctx context.Context, proposalName, step, _ string) (string, error) {
9191
log := logf.FromContext(ctx)
9292

93-
templateName, err := EnsureAgentTemplate(ctx, m.Client, m.BaseTemplateName, m.Namespace, step, m.agent, m.llm, m.tools, m.serviceAccount)
93+
templateName, err := EnsureAgentTemplate(ctx, m.Client, m.BaseTemplateName, m.Namespace, step, m.agent, m.llm, m.tools, nil, m.serviceAccount)
9494
if err != nil {
9595
return "", fmt.Errorf("ensure agent template: %w", err)
9696
}

controller/proposal/sandbox_templates.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,14 @@ var sandboxTemplateGVK = schema.GroupVersionKind{
2626
}
2727

2828
const (
29-
llmCredsMountPath = "/var/run/secrets/llm-credentials"
30-
llmCredsVolumeName = "llm-credentials"
31-
mcpHeadersMountRoot = "/var/secrets/mcp"
32-
mcpServersEnvVar = "LIGHTSPEED_MCP_SERVERS"
29+
agentModeEnvVar = "LIGHTSPEED_MODE"
30+
llmCredsMountPath = "/var/run/secrets/llm-credentials"
31+
vertexCredsMountPath = "/var/secrets/google"
32+
vertexCredsFileName = "credentials.json"
33+
llmCredsVolumeName = "llm-credentials"
34+
mcpHeadersMountRoot = "/var/secrets/mcp"
35+
mcpServersEnvVar = "LIGHTSPEED_MCP_SERVERS"
36+
dataSourceMountPath = "/data/input"
3337

3438
LabelManaged = "agentic.openshift.io/managed"
3539
LabelBaseTemplate = "agentic.openshift.io/base-template"
@@ -45,6 +49,7 @@ type templateHashInput struct {
4549
Skills []agenticv1alpha1.SkillsSource `json:"skills"`
4650
MCPServers []agenticv1alpha1.MCPServerConfig `json:"mcpServers,omitempty"`
4751
RequiredSecrets []agenticv1alpha1.SecretRequirement `json:"requiredSecrets,omitempty"`
52+
DataSource *agenticv1alpha1.DataSource `json:"dataSource,omitempty"`
4853
Step string `json:"step"`
4954
BaseResourceVersion string `json:"baseRV"`
5055
ServiceAccount string `json:"serviceAccount"`
@@ -56,6 +61,7 @@ func computeTemplateHash(
5661
skills []agenticv1alpha1.SkillsSource,
5762
mcpServers []agenticv1alpha1.MCPServerConfig,
5863
requiredSecrets []agenticv1alpha1.SecretRequirement,
64+
dataSource *agenticv1alpha1.DataSource,
5965
step string,
6066
baseResourceVersion string,
6167
serviceAccount string,
@@ -66,6 +72,7 @@ func computeTemplateHash(
6672
Skills: skills,
6773
MCPServers: mcpServers,
6874
RequiredSecrets: requiredSecrets,
75+
DataSource: dataSource,
6976
Step: step,
7077
BaseResourceVersion: baseResourceVersion,
7178
ServiceAccount: serviceAccount,
@@ -95,6 +102,7 @@ func EnsureAgentTemplate(
95102
agent *agenticv1alpha1.Agent,
96103
llm *agenticv1alpha1.LLMProvider,
97104
tools *agenticv1alpha1.ToolsSpec,
105+
dataSource *agenticv1alpha1.DataSource,
98106
serviceAccount string,
99107
) (string, error) {
100108
log := logf.FromContext(ctx).WithName("sandbox-templates")
@@ -121,7 +129,7 @@ func EnsureAgentTemplate(
121129
requiredSecrets = tools.RequiredSecrets
122130
}
123131

124-
hash, err := computeTemplateHash(llm, agent.Spec.Model, skills, mcpServers, requiredSecrets, step, base.GetResourceVersion(), serviceAccount)
132+
hash, err := computeTemplateHash(llm, agent.Spec.Model, skills, mcpServers, requiredSecrets, dataSource, step, base.GetResourceVersion(), serviceAccount)
125133
if err != nil {
126134
return "", fmt.Errorf("compute template hash: %w", err)
127135
}
@@ -187,6 +195,12 @@ func EnsureAgentTemplate(
187195
}
188196
}
189197

198+
if dataSource != nil {
199+
if err := patchDataSource(derived, dataSource); err != nil {
200+
return "", fmt.Errorf("patch data source: %w", err)
201+
}
202+
}
203+
190204
if err := patchProbes(derived); err != nil {
191205
return "", fmt.Errorf("patch probes: %w", err)
192206
}
@@ -569,6 +583,36 @@ func addSecretVolume(tmpl *unstructured.Unstructured, volumeName, secretName str
569583
return unstructured.SetNestedSlice(tmpl.Object, volumes, "spec", "podTemplate", "spec", "volumes")
570584
}
571585

586+
func addPVCVolume(tmpl *unstructured.Unstructured, volumeName, claimName string) error {
587+
volumes, _, _ := unstructured.NestedSlice(tmpl.Object, "spec", "podTemplate", "spec", "volumes")
588+
vol := map[string]any{
589+
"name": volumeName,
590+
"persistentVolumeClaim": map[string]any{
591+
"claimName": claimName,
592+
},
593+
}
594+
for i, v := range volumes {
595+
existing, ok := v.(map[string]any)
596+
if !ok {
597+
continue
598+
}
599+
if existing["name"] == volumeName {
600+
volumes[i] = vol
601+
return unstructured.SetNestedSlice(tmpl.Object, volumes, "spec", "podTemplate", "spec", "volumes")
602+
}
603+
}
604+
volumes = append(volumes, vol)
605+
return unstructured.SetNestedSlice(tmpl.Object, volumes, "spec", "podTemplate", "spec", "volumes")
606+
}
607+
608+
func patchDataSource(tmpl *unstructured.Unstructured, ds *agenticv1alpha1.DataSource) error {
609+
volName := "data-source"
610+
if err := addPVCVolume(tmpl, volName, ds.ClaimName); err != nil {
611+
return fmt.Errorf("add data source PVC volume: %w", err)
612+
}
613+
return addVolumeMount(tmpl, volName, dataSourceMountPath, true)
614+
}
615+
572616
func addVolumeMount(tmpl *unstructured.Unstructured, name, mountPath string, readOnly bool) error {
573617
container, containers, err := firstContainer(tmpl)
574618
if err != nil {

controller/proposal/sandbox_templates_test.go

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

7777
func mustHash(t *testing.T, llm *agenticv1alpha1.LLMProvider, model string, skills []agenticv1alpha1.SkillsSource, requiredSecrets []agenticv1alpha1.SecretRequirement, phase string) string {
7878
t.Helper()
79-
h, err := computeTemplateHash(llm, model, skills, nil, requiredSecrets, phase, "", "")
79+
h, err := computeTemplateHash(llm, model, skills, nil, requiredSecrets, nil, phase, "", "")
8080
if err != nil {
8181
t.Fatalf("computeTemplateHash: %v", err)
8282
}
@@ -516,14 +516,14 @@ func TestSetEnvVar_FailsOnNoContainers(t *testing.T) {
516516
}
517517

518518
func TestEnsureAgentTemplate_NilAgent(t *testing.T) {
519-
_, err := EnsureAgentTemplate(nil, nil, "base", "ns", "analysis", nil, testLLMProvider(agenticv1alpha1.LLMProviderGoogleCloudVertex), nil, "lightspeed-agent")
519+
_, err := EnsureAgentTemplate(nil, nil, "base", "ns", "analysis", nil, testLLMProvider(agenticv1alpha1.LLMProviderGoogleCloudVertex), nil, nil, "lightspeed-agent")
520520
if err == nil {
521521
t.Error("expected error for nil agent")
522522
}
523523
}
524524

525525
func TestEnsureAgentTemplate_NilLLM(t *testing.T) {
526-
_, err := EnsureAgentTemplate(nil, nil, "base", "ns", "analysis", testDefaultAgent(), nil, nil, "lightspeed-agent")
526+
_, err := EnsureAgentTemplate(nil, nil, "base", "ns", "analysis", testDefaultAgent(), nil, nil, nil, "lightspeed-agent")
527527
if err == nil {
528528
t.Error("expected error for nil LLM")
529529
}
@@ -626,11 +626,11 @@ func TestComputeTemplateHash_DifferentBaseResourceVersion(t *testing.T) {
626626
llm := testLLMProvider(agenticv1alpha1.LLMProviderGoogleCloudVertex)
627627
skills := []agenticv1alpha1.SkillsSource{{Image: "quay.io/test/skills:latest"}}
628628

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

647-
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "")
647+
h1, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, nil, "analysis", "1000", "")
648648
if err != nil {
649649
t.Fatal(err)
650650
}
651-
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, "analysis", "1000", "")
651+
h2, err := computeTemplateHash(llm, "claude-opus-4-6", skills, nil, nil, nil, "analysis", "1000", "")
652652
if err != nil {
653653
t.Fatal(err)
654654
}

controller/proposal/templates/analysis_query.tmpl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
You are an analysis agent. Your job is to diagnose the problem, determine the root cause, and propose one or more remediation options. Do NOT attempt to fix, patch, or execute any changes — only analyze and propose.
22

3+
## Skills
4+
5+
A specialist deep-RCA pipeline is available at `/app/skills/intelliaide/SKILL.md`.
6+
7+
Use it ONLY when the request calls for:
8+
- Root cause analysis (RCA) or deeper / ML-assisted troubleshooting of a cluster issue
9+
- Must-gather collection or analysis
10+
- Investigating pod failures, etcd degradation, networking problems, storage issues, etc.
11+
- Any request that explicitly mentions "deeper analysis", "deeper troubleshooting", "root cause", "RCA", "must-gather", or "IntelliAide"
12+
13+
For routine inspection (checking pod/node status, listing events, summarising resource state,
14+
describing objects), use `kubectl`/`oc` commands directly — do NOT invoke the IntelliAide pipeline.
15+
16+
**Decision rule — apply before doing anything else:**
17+
1. Read the `## Request` section below.
18+
2. If it is a routine inspection query → proceed with `kubectl`/`oc` directly.
19+
3. If it is a deep-RCA or troubleshooting request → read the skill file with ONE atomic command:
20+
```
21+
cat /app/skills/intelliaide/SKILL.md
22+
```
23+
If the command returns one or more paths, read the most relevant SKILL.md with `cat`
24+
and follow its workflow **exactly** instead of the instructions below.
25+
If no SKILL.md files are found, stop immediately and return a JSON error response — skills are required and their absence is a fatal misconfiguration.
26+
27+
## Analysis requirements
28+
329
For each option you propose, include:
430
- A diagnosis with root cause and confidence level
531
- A detailed remediation plan with specific actions

0 commit comments

Comments
 (0)