Skip to content

feat: add PodSpecBuilder for typed pod-spec assembly#85

Open
thoraxe wants to merge 1 commit into
openshift:mainfrom
thoraxe:bare-pod-1-podspec-builder
Open

feat: add PodSpecBuilder for typed pod-spec assembly#85
thoraxe wants to merge 1 commit into
openshift:mainfrom
thoraxe:bare-pod-1-podspec-builder

Conversation

@thoraxe
Copy link
Copy Markdown
Contributor

@thoraxe thoraxe commented Jun 5, 2026

Summary

  • Create PodSpecBuilder struct that assembles a typed corev1.PodSpec from agent container image and resolved step config
  • Handles LLM env vars, credential mounts, skills OCI volumes, MCP server config, required secrets, probes, and security context
  • Shared foundation for both SandboxClaim and bare pod sandbox modes

Jira: OLS-3242
Stacked PR: 1 of 5 — targets main

Test plan

  • 11 unit tests covering all LLM providers, skills, MCP, required secrets, error cases
  • Full proposal package tests pass
  • No regressions in existing tests

🤖 Generated with Claude Code

Extract pod-spec construction logic into a reusable PodSpecBuilder
struct that produces corev1.PodSpec. Handles LLM env vars, credential
mounts, skills OCI volumes, MCP server config, required secrets,
probes, and security context. Shared foundation for both SandboxClaim
and bare pod sandbox modes.

Ref: OLS-3242
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for multiple LLM providers (Anthropic, Vertex, Azure OpenAI, AWS Bedrock, OpenAI) in agent configuration
    • Enabled tool integrations with skills and Model Context Protocol servers
    • Added secrets management and environment variable injection for agent setup
  • Tests

    • Added comprehensive test coverage for all LLM provider configurations and agent deployment scenarios

Walkthrough

This PR introduces PodSpecBuilder, a new component that constructs Kubernetes PodSpecs for agent containers. It configures container image, security context, port/HTTP probes, injects provider/model environment variables and credential secrets, and conditionally adds volumes/mounts for optional tool integrations (skills, MCP servers, required secrets).

Changes

PodSpec builder implementation and tests

Layer / File(s) Summary
PodSpec builder type and main orchestration
controller/proposal/podspec_builder.go
PodSpecBuilder type with Image field. Build method validates inputs, configures the agent container (image, security context, port 8080, readiness/liveness HTTP probes), injects provider/model env vars and credential secret env/volume mounts, then conditionally extends the pod with tool-related volumes/mounts/env.
Provider-specific environment configuration
controller/proposal/podspec_builder.go
addProviderSpecificEnv sets provider-dependent env vars (Anthropic, Vertex with project/region/model, Azure OpenAI with API version, AWS Bedrock with region, OpenAI with custom URLs).
Tool integration implementations
controller/proposal/podspec_builder.go
buildSkills creates image-backed volumes and per-file mounts. buildMCPServers marshals MCP configs to JSON env var and creates secret-backed volumes/mounts for MCP headers. buildRequiredSecrets converts requirements into volume/mounts or secret-backed env vars.
Core provider and validation tests
controller/proposal/podspec_builder_test.go
Test helpers and positive tests for Anthropic, Vertex, Azure, Bedrock, and OpenAI provider configurations; negative tests asserting Build fails when agent or LLMProvider is nil with specific error messages.
Tool integration tests
controller/proposal/podspec_builder_test.go
Tests validating skills volumes with and without per-file subpaths, required secrets as environment variables with secretKeyRef, and required secrets as read-only file mounts with Kubernetes secret volumes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add PodSpecBuilder for typed pod-spec assembly' clearly summarizes the main change: introducing a new PodSpecBuilder component for assembling Kubernetes PodSpec objects.
Description check ✅ Passed The description is directly related to the changeset, detailing the PodSpecBuilder implementation, its responsibilities, test coverage, and Jira tracking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci openshift-ci Bot requested review from blublinsky and onmete June 5, 2026 21:19
@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Jun 5, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign xrajesh for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci
Copy link
Copy Markdown

openshift-ci Bot commented Jun 5, 2026

@thoraxe: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
controller/proposal/podspec_builder.go (3)

20-25: ⚡ Quick win

Unused step parameter.

The step string parameter is accepted but never used. Remove it or use it.

 func (b *PodSpecBuilder) Build(
 	agent *agenticv1alpha1.Agent,
 	llm *agenticv1alpha1.LLMProvider,
 	tools *agenticv1alpha1.ToolsSpec,
-	step string,
 ) (*corev1.PodSpec, error) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 20 - 25, The Build
method on PodSpecBuilder declares an unused parameter "step string" — remove the
unused parameter from the PodSpecBuilder.Build signature and update any callers
to stop passing the step argument (or if the step is intended to influence pod
creation, thread it into the function body and use it where appropriate); locate
the method by name PodSpecBuilder.Build and any call sites that pass a step
value and either delete the argument at those call sites or implement usage
inside Build (e.g., selecting containers/labels/annotations based on step) so
the parameter is no longer unused.

26-31: ⚡ Quick win

Error strings should use const pattern.

Per coding guidelines: define errors using const ErrFoo = "…" and wrap with fmt.Errorf("%s: %w", …).

Proposed fix
+const (
+	ErrAgentRequired      = "agent is required"
+	ErrLLMProviderRequired = "LLMProvider is required"
+)
+
 func (b *PodSpecBuilder) Build(
 ...
 	if agent == nil {
-		return nil, fmt.Errorf("agent is required")
+		return nil, fmt.Errorf(ErrAgentRequired)
 	}
 	if llm == nil {
-		return nil, fmt.Errorf("LLMProvider is required")
+		return nil, fmt.Errorf(ErrLLMProviderRequired)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 26 - 31, Define constant
error messages (e.g. const ErrAgentRequired = "agent is required" and const
ErrLLMRequired = "LLMProvider is required") and replace the inline fmt.Errorf
calls in the nil checks for agent and llm with returns that use those constants
(e.g. return nil, fmt.Errorf("%s", ErrAgentRequired) or
errors.New(ErrAgentRequired)); update the nil-checks referencing variables agent
and llm in podspec_builder (the constructor/initializer function containing
those checks) to use the new consts so error strings follow the const pattern.

160-164: ⚡ Quick win

Multiple skills entries aren’t a bug here—they’re intentionally “first non-empty only.”

buildSkills only uses skills[0], and the project spec explicitly documents that when multiple skills entries exist in ToolsSpec, template derivation applies image/path patching based on the first non-empty skills source (current behavior). Multi-image support is tracked as a planned change (OLS-2894).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 160 - 164, buildSkills
currently assumes the first element is the active source by directly using
skills[0], which hides the intended "first non-empty wins" behavior; update
buildSkills to explicitly iterate the skills slice to find the first
agenticv1alpha1.SkillsSource with a non-empty Image (assign that to s) and fall
back to returning nil,nil if none found, leaving the overall semantics as "first
non-empty only" (no multi-image support) so callers and future readers see the
intent clearly; refer to the buildSkills function and the SkillsSource type when
making this change.
controller/proposal/podspec_builder_test.go (1)

12-19: 💤 Low value

envToMap silently ignores ValueFrom env vars.

This helper only captures env.Value, not env.ValueFrom. Tests relying on this helper won't catch secret-backed env vars. The TestPodSpecBuilder_RequiredSecrets_EnvVar test correctly iterates manually, but adding a comment or separate helper would prevent future misuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder_test.go` around lines 12 - 19, The helper
envToMap currently only reads env.Value and ignores env.ValueFrom, which hides
secret-backed env vars; update envToMap (or add a new helper
envToMapIncludeValueFrom) to detect when corev1.EnvVar.ValueFrom is non-nil and
represent it in the map (e.g., set the map entry to a deterministic descriptive
string containing the ValueFrom specifics such as SecretKeyRef.Name and
SecretKeyRef.Key or a fixed tag like "<ValueFrom:Secret:NAME:KEY>") so tests
like TestPodSpecBuilder_RequiredSecrets_EnvVar can assert secret-backed env vars
rather than missing them. Ensure you reference the envToMap (or new helper) and
ValueFrom/SecretKeyRef fields when locating the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/proposal/podspec_builder.go`:
- Around line 176-189: When s.Paths is empty you currently create the skills
volume but never add any VolumeMounts, so the container can't access the image
content; update the podspec_builder logic that builds mounts (the mounts slice
loop around s.Paths and the baseMountPath variable) to add a fallback
VolumeMount that mounts the entire skills volume at baseMountPath (e.g.,
MountPath set to baseMountPath, Name "skills", no SubPath or empty SubPath,
ReadOnly true) when len(s.Paths) == 0 so the whole image is available to the
container.
- Around line 210-223: The code appends a corev1.Volume for each MCP header that
uses a secret, causing duplicate volumes when multiple headers reference the
same secret; fix by adding a seen set (e.g., map[string]bool or
map[string]struct{}) keyed by the secret name before the header loop and only
append to volumes when the secret name (h.ValueFrom.Secret.Name) has not been
seen yet, then mark it seen; likewise avoid duplicate mounts by checking the
volName or secret name against a seenMounts set before appending to mounts;
apply this logic where MCPHeaderSourceTypeSecret is checked (symbols: h, he,
volumes, mounts, volName, mcpHeadersMountRoot, MCPHeaderSourceTypeSecret) so
duplicate volume/mount entries are not created.
- Around line 264-270: The code hardcodes SecretKeySelector.Key = "token"
causing silent misses; update the API and usage so the secret data key is
configurable: add a Key string field to SecretMountEnvVarConfig in
api/v1alpha1/tools_types.go (with CRD/json tags and a sensible default), change
the podspec builder to read that field instead of the literal "token" (fall back
to "token" when the field is empty), and update any constructors/decoding sites
that create SecretMountEnvVarConfig so the new field is populated or defaulted
appropriately.

---

Nitpick comments:
In `@controller/proposal/podspec_builder_test.go`:
- Around line 12-19: The helper envToMap currently only reads env.Value and
ignores env.ValueFrom, which hides secret-backed env vars; update envToMap (or
add a new helper envToMapIncludeValueFrom) to detect when
corev1.EnvVar.ValueFrom is non-nil and represent it in the map (e.g., set the
map entry to a deterministic descriptive string containing the ValueFrom
specifics such as SecretKeyRef.Name and SecretKeyRef.Key or a fixed tag like
"<ValueFrom:Secret:NAME:KEY>") so tests like
TestPodSpecBuilder_RequiredSecrets_EnvVar can assert secret-backed env vars
rather than missing them. Ensure you reference the envToMap (or new helper) and
ValueFrom/SecretKeyRef fields when locating the code to change.

In `@controller/proposal/podspec_builder.go`:
- Around line 20-25: The Build method on PodSpecBuilder declares an unused
parameter "step string" — remove the unused parameter from the
PodSpecBuilder.Build signature and update any callers to stop passing the step
argument (or if the step is intended to influence pod creation, thread it into
the function body and use it where appropriate); locate the method by name
PodSpecBuilder.Build and any call sites that pass a step value and either delete
the argument at those call sites or implement usage inside Build (e.g.,
selecting containers/labels/annotations based on step) so the parameter is no
longer unused.
- Around line 26-31: Define constant error messages (e.g. const ErrAgentRequired
= "agent is required" and const ErrLLMRequired = "LLMProvider is required") and
replace the inline fmt.Errorf calls in the nil checks for agent and llm with
returns that use those constants (e.g. return nil, fmt.Errorf("%s",
ErrAgentRequired) or errors.New(ErrAgentRequired)); update the nil-checks
referencing variables agent and llm in podspec_builder (the
constructor/initializer function containing those checks) to use the new consts
so error strings follow the const pattern.
- Around line 160-164: buildSkills currently assumes the first element is the
active source by directly using skills[0], which hides the intended "first
non-empty wins" behavior; update buildSkills to explicitly iterate the skills
slice to find the first agenticv1alpha1.SkillsSource with a non-empty Image
(assign that to s) and fall back to returning nil,nil if none found, leaving the
overall semantics as "first non-empty only" (no multi-image support) so callers
and future readers see the intent clearly; refer to the buildSkills function and
the SkillsSource type when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6723b519-c2ed-40f7-a405-0de520fabcc0

📥 Commits

Reviewing files that changed from the base of the PR and between b527722 and 547733a.

📒 Files selected for processing (2)
  • controller/proposal/podspec_builder.go
  • controller/proposal/podspec_builder_test.go

Comment on lines +176 to +189
var mounts []corev1.VolumeMount
if len(s.Paths) > 0 {
baseMountPath := "/app/skills"
for _, p := range s.Paths {
subPath := strings.TrimPrefix(p, "/")
skillName := path.Base(p)
mounts = append(mounts, corev1.VolumeMount{
Name: "skills",
MountPath: path.Join(baseMountPath, skillName),
SubPath: subPath,
ReadOnly: true,
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No volume mounts when Paths is empty.

When skills[0].Paths is empty, a volume is created but no mounts are added. The container can't access the skills image content. Either mount the entire image at a base path, or document this as intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 176 - 189, When s.Paths
is empty you currently create the skills volume but never add any VolumeMounts,
so the container can't access the image content; update the podspec_builder
logic that builds mounts (the mounts slice loop around s.Paths and the
baseMountPath variable) to add a fallback VolumeMount that mounts the entire
skills volume at baseMountPath (e.g., MountPath set to baseMountPath, Name
"skills", no SubPath or empty SubPath, ReadOnly true) when len(s.Paths) == 0 so
the whole image is available to the container.

Comment on lines +210 to +223
if h.ValueFrom.Type == agenticv1alpha1.MCPHeaderSourceTypeSecret {
he.SecretName = h.ValueFrom.Secret.Name
volName := "mcp-header-" + h.ValueFrom.Secret.Name
volumes = append(volumes, corev1.Volume{
Name: volName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{SecretName: h.ValueFrom.Secret.Name},
},
})
mounts = append(mounts, corev1.VolumeMount{
Name: volName,
MountPath: mcpHeadersMountRoot + "/" + h.ValueFrom.Secret.Name,
ReadOnly: true,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Duplicate volumes if same secret used in multiple MCP headers.

If two MCP servers reference the same secret for headers, this creates duplicate volume entries with the same name, causing pod creation to fail.

Proposed fix: deduplicate by tracking seen secrets
+	seenSecrets := make(map[string]bool)
 	for _, s := range servers {
 		entry := mcpServerEnvEntry{...}
 		for _, h := range s.Headers {
 			he := mcpHeaderEnvEntry{...}
 			if h.ValueFrom.Type == agenticv1alpha1.MCPHeaderSourceTypeSecret {
 				he.SecretName = h.ValueFrom.Secret.Name
 				volName := "mcp-header-" + h.ValueFrom.Secret.Name
+				if !seenSecrets[h.ValueFrom.Secret.Name] {
+					seenSecrets[h.ValueFrom.Secret.Name] = true
 					volumes = append(volumes, corev1.Volume{...})
 					mounts = append(mounts, corev1.VolumeMount{...})
+				}
 			}
 			entry.Headers = append(entry.Headers, he)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 210 - 223, The code
appends a corev1.Volume for each MCP header that uses a secret, causing
duplicate volumes when multiple headers reference the same secret; fix by adding
a seen set (e.g., map[string]bool or map[string]struct{}) keyed by the secret
name before the header loop and only append to volumes when the secret name
(h.ValueFrom.Secret.Name) has not been seen yet, then mark it seen; likewise
avoid duplicate mounts by checking the volName or secret name against a
seenMounts set before appending to mounts; apply this logic where
MCPHeaderSourceTypeSecret is checked (symbols: h, he, volumes, mounts, volName,
mcpHeadersMountRoot, MCPHeaderSourceTypeSecret) so duplicate volume/mount
entries are not created.

Comment on lines +264 to +270
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: s.Name},
Key: "token",
Optional: &optional,
},
},
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the SecretMountEnvVarConfig type definition for available fields
ast-grep --pattern 'type SecretMountEnvVarConfig struct {
  $$$
}'

Repository: openshift/lightspeed-agentic-operator

Length of output: 944


Hardcoded "token" secret data key for env-var mounts (potential silent env injection failure)

controller/proposal/podspec_builder.go always sets SecretKeyRef.Key to "token" for env-var secret mounts, but api/v1alpha1/tools_types.go’s SecretMountEnvVarConfig only exposes Name (no configurable secret data key). With Optional: true, a missing/mismatched "token" entry won’t fail—env vars may not be populated. Add a configurable key field (or clearly document that secret data["token"] is mandatory).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/podspec_builder.go` around lines 264 - 270, The code
hardcodes SecretKeySelector.Key = "token" causing silent misses; update the API
and usage so the secret data key is configurable: add a Key string field to
SecretMountEnvVarConfig in api/v1alpha1/tools_types.go (with CRD/json tags and a
sensible default), change the podspec builder to read that field instead of the
literal "token" (fall back to "token" when the field is empty), and update any
constructors/decoding sites that create SecretMountEnvVarConfig so the new field
is populated or defaulted appropriately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant