From 55dd6efaae5d0de6f8c358ca28f7121d072a3d56 Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Tue, 12 May 2026 14:28:46 -0400 Subject: [PATCH] Extract controller setup, add console/sandbox image flags, and refine API types - Extract controller wiring into controller/setup.go (Options struct with Namespace, AgenticConsoleImage, AgenticSandboxImage) - Add --agentic-console-image and --agentic-sandbox-image CLI flags to main.go, replacing hardcoded template references - Register OpenShift console/operator API schemes for ConsolePlugin reconciliation - Refine SkillsSource docs and make paths required (no whole-image mount) - Add defaultTools to Proposal spec for per-proposal tool defaults - Simplify sandbox agent caller setup - Update examples and docs to use find-token skill from quay.io/harpatil/agentic-skills:latest (TODO: replace with Konflux image) Co-Authored-By: Claude Opus 4.6 (1M context) --- api/v1alpha1/proposal_types.go | 4 +- api/v1alpha1/shared_types.go | 24 +-- api/v1alpha1/tools_types.go | 6 +- cmd/main.go | 44 ++--- .../bases/agentic.openshift.io_proposals.yaml | 175 ++++++++++-------- controller/console/reconciler_test.go | 4 +- controller/proposal/handlers.go | 6 + controller/proposal/reconciler.go | 8 +- controller/proposal/reconciler_test.go | 7 +- controller/proposal/sandbox_agent.go | 31 ++-- controller/proposal/sandbox_agent_test.go | 2 - controller/setup.go | 50 +++++ examples/setup/02-approval-policy.yaml | 11 +- examples/setup/03-proposals.yaml | 25 ++- examples/setup/04-acs-proposals.yaml | 21 ++- examples/setup/05-alertmanager-proposals.yaml | 10 +- examples/setup/06-ossm-proposals.yaml | 10 +- examples/setup/07-assisted-proposals.yaml | 10 +- examples/setup/08-mcp-demo.yaml | 5 +- 19 files changed, 277 insertions(+), 176 deletions(-) create mode 100644 controller/setup.go diff --git a/api/v1alpha1/proposal_types.go b/api/v1alpha1/proposal_types.go index c42a156..9efbdea 100644 --- a/api/v1alpha1/proposal_types.go +++ b/api/v1alpha1/proposal_types.go @@ -422,7 +422,7 @@ type ProposalStatus struct { // - lightspeed-demo // tools: // skills: -// - image: registry.redhat.io/acs/acs-lightspeed-skills:latest +// - image: registry.redhat.io/acs/acs-agentic-skills:latest // analysis: // agent: smart // @@ -439,7 +439,7 @@ type ProposalStatus struct { // - lightspeed-demo // tools: // skills: -// - image: registry.redhat.io/acs/acs-lightspeed-skills:latest +// - image: registry.redhat.io/acs/acs-agentic-skills:latest // requiredSecrets: // - name: acs-api-token // mountAs: diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index 7788530..753e8da 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -135,25 +135,18 @@ type MCPServerConfig struct { Headers []MCPHeader `json:"headers,omitempty"` } -// SkillsSource defines an OCI image containing skills and optionally which -// paths within that image to mount. Skills are mounted as Kubernetes image +// SkillsSource defines an OCI image containing skills and which paths +// within that image to mount. Skills are mounted as Kubernetes image // volumes in the agent's sandbox pod. // -// When paths is omitted, the entire image is mounted. When paths is specified, -// only those directories are mounted (each as a separate subPath volumeMount), -// allowing selective composition of skills from large shared images. +// Each path is mounted as a separate subPath volumeMount, allowing +// selective composition of skills from shared images. // -// Example — mount all skills from a custom image: -// -// skills: -// - image: quay.io/my-org/my-skills:latest -// -// Example — selectively mount two skills from a shared image: +// Example — mount specific skills from the agentic-skills image: // // skills: // - image: registry.ci.openshift.org/ocp/5.0:agentic-skills // paths: -// - /skills/prometheus // - /skills/cluster-update/update-advisor type SkillsSource struct { // image is the OCI image reference containing skills. @@ -174,7 +167,7 @@ type SkillsSource struct { // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != '' ? self.find(':.*$').matches(':[0-9A-Fa-f]*$') : true",message="digest must only contain hex characters (A-F, a-f, 0-9)" Image string `json:"image,omitempty"` - // paths restricts which directories from the image are mounted. + // paths specifies which directories from the image are mounted. // Each path is mounted as a separate subPath volumeMount into the agent's // skills directory. The last segment of each path becomes the mount name // (e.g., "/skills/prometheus" mounts as "prometheus"). @@ -183,9 +176,8 @@ type SkillsSource struct { // or "." segments, no double slashes, no trailing slash, and only // alphanumeric characters, hyphens, underscores, dots, and slashes. // - // When omitted, the entire image is mounted as a single volume. // Maximum 50 items. - // +optional + // +required // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=50 @@ -197,5 +189,5 @@ type SkillsSource struct { // +kubebuilder:validation:XValidation:rule="self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$'))",message="paths may only contain alphanumeric characters, '/', '_', '.', and '-'" // +kubebuilder:validation:items:MinLength=2 // +kubebuilder:validation:items:MaxLength=512 - Paths []string `json:"paths,omitempty"` + Paths []string `json:"paths"` } diff --git a/api/v1alpha1/tools_types.go b/api/v1alpha1/tools_types.go index 5843d10..4955d28 100644 --- a/api/v1alpha1/tools_types.go +++ b/api/v1alpha1/tools_types.go @@ -85,10 +85,10 @@ type SecretMountSpec struct { } // SecretRequirement declares a Kubernetes Secret that the sandbox needs -// at runtime. The cluster admin creates the actual Secret in the same -// namespace as the Proposal. +// at runtime. The Secret must exist in the operator namespace (where +// sandbox pods run), not in the Proposal's namespace. type SecretRequirement struct { - // name of the Secret (must exist in the same namespace as the Proposal). + // name of the Secret (must exist in the operator namespace). // Must be a valid RFC 1123 DNS subdomain. // +required // +kubebuilder:validation:MinLength=1 diff --git a/cmd/main.go b/cmd/main.go index 4cd069e..d794ee6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,8 @@ import ( "flag" "os" + consolev1 "github.com/openshift/api/console/v1" + openshiftv1 "github.com/openshift/api/operator/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -13,7 +15,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" - "github.com/openshift/lightspeed-agentic-operator/controller/proposal" + agenticcontroller "github.com/openshift/lightspeed-agentic-operator/controller" ) var scheme = runtime.NewScheme() @@ -21,20 +23,24 @@ var scheme = runtime.NewScheme() func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(agenticv1alpha1.AddToScheme(scheme)) + utilruntime.Must(consolev1.AddToScheme(scheme)) + utilruntime.Must(openshiftv1.AddToScheme(scheme)) } func main() { var ( - metricsAddr string - healthAddr string - namespace string - templateName string + metricsAddr string + healthAddr string + namespace string + agenticConsoleImage string + agenticSandboxImage string ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&healthAddr, "health-probe-bind-address", ":8081", "The address the health probe endpoint binds to.") flag.StringVar(&namespace, "namespace", "", "The namespace where the operator runs (required).") - flag.StringVar(&templateName, "template-name", "lightspeed-agent", "Default SandboxTemplate name.") + flag.StringVar(&agenticConsoleImage, "agentic-console-image", "", "The image of the agentic console plugin container.") + flag.StringVar(&agenticSandboxImage, "agentic-sandbox-image", "", "The image of the agentic sandbox container.") flag.Parse() ctrl.SetLogger(zap.New(zap.UseDevMode(true))) @@ -59,24 +65,12 @@ func main() { os.Exit(1) } - sandboxMgr := proposal.NewSandboxManager(mgr.GetClient(), namespace) - - agentCaller := proposal.NewSandboxAgentCaller( - sandboxMgr, - mgr.GetClient(), - proposal.NewAgentHTTPClient, - namespace, - templateName, - ) - - reconciler := &proposal.ProposalReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Proposal"), - Agent: agentCaller, - } - - if err := reconciler.SetupWithManager(mgr); err != nil { - log.Error(err, "unable to create controller", "controller", "Proposal") + if err := agenticcontroller.Setup(mgr, agenticcontroller.Options{ + Namespace: namespace, + AgenticConsoleImage: agenticConsoleImage, + AgenticSandboxImage: agenticSandboxImage, + }); err != nil { + log.Error(err, "unable to set up agentic controllers") os.Exit(1) } @@ -89,7 +83,7 @@ func main() { os.Exit(1) } - log.Info("starting manager", "namespace", namespace, "template", templateName) + log.Info("starting manager", "namespace", namespace) if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { log.Error(err, "problem running manager") os.Exit(1) diff --git a/config/crd/bases/agentic.openshift.io_proposals.yaml b/config/crd/bases/agentic.openshift.io_proposals.yaml index ea45f81..c6dadf1 100644 --- a/config/crd/bases/agentic.openshift.io_proposals.yaml +++ b/config/crd/bases/agentic.openshift.io_proposals.yaml @@ -33,13 +33,13 @@ spec: agentic.openshift.io/v1alpha1\n\tkind: Proposal\n\tmetadata:\n\t name: one-off-investigation\n\tspec:\n\t request: \"Investigate why pod foo is crashlooping\"\n\t targetNamespaces:\n\t - lightspeed-demo\n\t tools:\n\t - \ skills:\n\t - image: registry.redhat.io/acs/acs-lightspeed-skills:latest\n\t + \ skills:\n\t - image: registry.redhat.io/acs/acs-agentic-skills:latest\n\t \ analysis:\n\t agent: smart\n\nExample — full remediation (analyze → execute → verify):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: Proposal\n\tmetadata:\n\t name: fix-nginx-cve-2024-1234\n\t namespace: stackrox\n\tspec:\n\t request: \"Fix CVE-2024-1234 in nginx:1.21\"\n\t \ targetNamespaces:\n\t - lightspeed-demo\n\t tools:\n\t skills:\n\t - \ - image: registry.redhat.io/acs/acs-lightspeed-skills:latest\n\t requiredSecrets:\n\t + \ - image: registry.redhat.io/acs/acs-agentic-skills:latest\n\t requiredSecrets:\n\t \ - name: acs-api-token\n\t mountAs:\n\t type: EnvVar\n\t \ envVar:\n\t name: ACS_API_TOKEN\n\t analysis:\n\t \ agent: smart\n\t execution: {}\n\t verification:\n\t agent: fast" @@ -234,8 +234,8 @@ spec: items: description: |- SecretRequirement declares a Kubernetes Secret that the sandbox needs - at runtime. The cluster admin creates the actual Secret in the same - namespace as the Proposal. + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. properties: description: description: |- @@ -316,7 +316,7 @@ spec: : !has(self.filePath)' name: description: |- - name of the Secret (must exist in the same namespace as the Proposal). + name of the Secret (must exist in the operator namespace). Must be a valid RFC 1123 DNS subdomain. maxLength: 253 minLength: 1 @@ -343,17 +343,14 @@ spec: skills directory. Each image must be unique within the list. items: description: "SkillsSource defines an OCI image containing - skills and optionally which\npaths within that image to - mount. Skills are mounted as Kubernetes image\nvolumes - in the agent's sandbox pod.\n\nWhen paths is omitted, - the entire image is mounted. When paths is specified,\nonly - those directories are mounted (each as a separate subPath - volumeMount),\nallowing selective composition of skills - from large shared images.\n\nExample — mount all skills - from a custom image:\n\n\tskills:\n\t - image: quay.io/my-org/my-skills:latest\n\nExample - — selectively mount two skills from a shared image:\n\n\tskills:\n\t - \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t - \ paths:\n\t - /skills/prometheus\n\t - /skills/cluster-update/update-advisor" + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" properties: image: description: |- @@ -403,7 +400,7 @@ spec: : true' paths: description: |- - paths restricts which directories from the image are mounted. + paths specifies which directories from the image are mounted. Each path is mounted as a separate subPath volumeMount into the agent's skills directory. The last segment of each path becomes the mount name (e.g., "/skills/prometheus" mounts as "prometheus"). @@ -412,7 +409,6 @@ spec: or "." segments, no double slashes, no trailing slash, and only alphanumeric characters, hyphens, underscores, dots, and slashes. - When omitted, the entire image is mounted as a single volume. Maximum 50 items. items: maxLength: 512 @@ -439,6 +435,7 @@ spec: rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) required: - image + - paths type: object maxItems: 20 minItems: 1 @@ -448,6 +445,42 @@ spec: x-kubernetes-list-type: map type: object type: object + analysisOutput: + description: |- + analysisOutput configures the analysis step's structured output. + The mode field controls which built-in properties are included + (Default: all; Minimal: only title). The schema field optionally + defines adapter-specific structured data injected as "components". + + When omitted, the analysis uses the full default schema with all + built-in properties and no custom components. + + Immutable: the output contract is fixed at creation. + minProperties: 1 + properties: + mode: + default: Default + description: |- + mode controls which built-in properties the analysis output schema + includes. Default includes all built-in properties (diagnosis, + proposal, summary, rbac, verification). Minimal includes only the + base structure (options array with title per option). Omit or set + to "Default" for standard remediation workflows. + enum: + - Default + - Minimal + type: string + schema: + description: |- + schema is a JSON Schema injected as a required "components" + property in each analysis output option. Use this to require + adapter-specific structured data beyond the base analysis schema. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-validations: + - message: schema is required when mode is Minimal + rule: self.mode != 'Minimal' || has(self.schema) execution: description: |- execution defines per-step configuration for the execution step. @@ -618,8 +651,8 @@ spec: items: description: |- SecretRequirement declares a Kubernetes Secret that the sandbox needs - at runtime. The cluster admin creates the actual Secret in the same - namespace as the Proposal. + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. properties: description: description: |- @@ -700,7 +733,7 @@ spec: : !has(self.filePath)' name: description: |- - name of the Secret (must exist in the same namespace as the Proposal). + name of the Secret (must exist in the operator namespace). Must be a valid RFC 1123 DNS subdomain. maxLength: 253 minLength: 1 @@ -727,17 +760,14 @@ spec: skills directory. Each image must be unique within the list. items: description: "SkillsSource defines an OCI image containing - skills and optionally which\npaths within that image to - mount. Skills are mounted as Kubernetes image\nvolumes - in the agent's sandbox pod.\n\nWhen paths is omitted, - the entire image is mounted. When paths is specified,\nonly - those directories are mounted (each as a separate subPath - volumeMount),\nallowing selective composition of skills - from large shared images.\n\nExample — mount all skills - from a custom image:\n\n\tskills:\n\t - image: quay.io/my-org/my-skills:latest\n\nExample - — selectively mount two skills from a shared image:\n\n\tskills:\n\t - \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t - \ paths:\n\t - /skills/prometheus\n\t - /skills/cluster-update/update-advisor" + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" properties: image: description: |- @@ -787,7 +817,7 @@ spec: : true' paths: description: |- - paths restricts which directories from the image are mounted. + paths specifies which directories from the image are mounted. Each path is mounted as a separate subPath volumeMount into the agent's skills directory. The last segment of each path becomes the mount name (e.g., "/skills/prometheus" mounts as "prometheus"). @@ -796,7 +826,6 @@ spec: or "." segments, no double slashes, no trailing slash, and only alphanumeric characters, hyphens, underscores, dots, and slashes. - When omitted, the entire image is mounted as a single volume. Maximum 50 items. items: maxLength: 512 @@ -823,6 +852,7 @@ spec: rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) required: - image + - paths type: object maxItems: 20 minItems: 1 @@ -832,16 +862,6 @@ spec: x-kubernetes-list-type: map type: object type: object - outputSchema: - description: |- - outputSchema is a JSON Schema injected as a required "components" - property in the analysis output. Use this to require adapter-specific - structured data beyond the base analysis schema (diagnosis, proposal, - RBAC, verification plan). - - Immutable: the output contract is fixed at creation. - type: object - x-kubernetes-preserve-unknown-fields: true request: description: |- request is the user's original request, alert description, or a @@ -1048,8 +1068,8 @@ spec: items: description: |- SecretRequirement declares a Kubernetes Secret that the sandbox needs - at runtime. The cluster admin creates the actual Secret in the same - namespace as the Proposal. + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. properties: description: description: |- @@ -1129,7 +1149,7 @@ spec: : !has(self.filePath)' name: description: |- - name of the Secret (must exist in the same namespace as the Proposal). + name of the Secret (must exist in the operator namespace). Must be a valid RFC 1123 DNS subdomain. maxLength: 253 minLength: 1 @@ -1156,17 +1176,13 @@ spec: skills directory. Each image must be unique within the list. items: description: "SkillsSource defines an OCI image containing skills - and optionally which\npaths within that image to mount. Skills - are mounted as Kubernetes image\nvolumes in the agent's sandbox - pod.\n\nWhen paths is omitted, the entire image is mounted. - When paths is specified,\nonly those directories are mounted - (each as a separate subPath volumeMount),\nallowing selective - composition of skills from large shared images.\n\nExample - — mount all skills from a custom image:\n\n\tskills:\n\t - - image: quay.io/my-org/my-skills:latest\n\nExample — selectively - mount two skills from a shared image:\n\n\tskills:\n\t - - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t - \ paths:\n\t - /skills/prometheus\n\t - /skills/cluster-update/update-advisor" + and which paths\nwithin that image to mount. Skills are mounted + as Kubernetes image\nvolumes in the agent's sandbox pod.\n\nEach + path is mounted as a separate subPath volumeMount, allowing\nselective + composition of skills from shared images.\n\nExample — mount + specific skills from the agentic-skills image:\n\n\tskills:\n\t + \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t + \ paths:\n\t - /skills/cluster-update/update-advisor" properties: image: description: |- @@ -1215,7 +1231,7 @@ spec: : true' paths: description: |- - paths restricts which directories from the image are mounted. + paths specifies which directories from the image are mounted. Each path is mounted as a separate subPath volumeMount into the agent's skills directory. The last segment of each path becomes the mount name (e.g., "/skills/prometheus" mounts as "prometheus"). @@ -1224,7 +1240,6 @@ spec: or "." segments, no double slashes, no trailing slash, and only alphanumeric characters, hyphens, underscores, dots, and slashes. - When omitted, the entire image is mounted as a single volume. Maximum 50 items. items: maxLength: 512 @@ -1251,6 +1266,7 @@ spec: rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) required: - image + - paths type: object maxItems: 20 minItems: 1 @@ -1429,8 +1445,8 @@ spec: items: description: |- SecretRequirement declares a Kubernetes Secret that the sandbox needs - at runtime. The cluster admin creates the actual Secret in the same - namespace as the Proposal. + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. properties: description: description: |- @@ -1511,7 +1527,7 @@ spec: : !has(self.filePath)' name: description: |- - name of the Secret (must exist in the same namespace as the Proposal). + name of the Secret (must exist in the operator namespace). Must be a valid RFC 1123 DNS subdomain. maxLength: 253 minLength: 1 @@ -1538,17 +1554,14 @@ spec: skills directory. Each image must be unique within the list. items: description: "SkillsSource defines an OCI image containing - skills and optionally which\npaths within that image to - mount. Skills are mounted as Kubernetes image\nvolumes - in the agent's sandbox pod.\n\nWhen paths is omitted, - the entire image is mounted. When paths is specified,\nonly - those directories are mounted (each as a separate subPath - volumeMount),\nallowing selective composition of skills - from large shared images.\n\nExample — mount all skills - from a custom image:\n\n\tskills:\n\t - image: quay.io/my-org/my-skills:latest\n\nExample - — selectively mount two skills from a shared image:\n\n\tskills:\n\t - \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t - \ paths:\n\t - /skills/prometheus\n\t - /skills/cluster-update/update-advisor" + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" properties: image: description: |- @@ -1598,7 +1611,7 @@ spec: : true' paths: description: |- - paths restricts which directories from the image are mounted. + paths specifies which directories from the image are mounted. Each path is mounted as a separate subPath volumeMount into the agent's skills directory. The last segment of each path becomes the mount name (e.g., "/skills/prometheus" mounts as "prometheus"). @@ -1607,7 +1620,6 @@ spec: or "." segments, no double slashes, no trailing slash, and only alphanumeric characters, hyphens, underscores, dots, and slashes. - When omitted, the entire image is mounted as a single volume. Maximum 50 items. items: maxLength: 512 @@ -1634,6 +1646,7 @@ spec: rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) required: - image + - paths type: object maxItems: 20 minItems: 1 @@ -1653,9 +1666,13 @@ spec: - message: targetNamespaces is immutable once set rule: '!has(oldSelf.targetNamespaces) || (has(self.targetNamespaces) && self.targetNamespaces == oldSelf.targetNamespaces)' - - message: outputSchema is immutable once set - rule: '!has(oldSelf.outputSchema) || (has(self.outputSchema) && self.outputSchema - == oldSelf.outputSchema)' + - message: analysisOutput is immutable once set + rule: '!has(oldSelf.analysisOutput) || (has(self.analysisOutput) && + self.analysisOutput == oldSelf.analysisOutput)' + - message: analysisOutput mode Minimal is only allowed for analysis-only + proposals (no execution or verification steps) + rule: '!has(self.analysisOutput) || self.analysisOutput.mode != ''Minimal'' + || (!has(self.execution) && !has(self.verification))' - message: tools is immutable once set rule: '!has(oldSelf.tools) || (has(self.tools) && self.tools == oldSelf.tools)' - message: analysis is immutable once set diff --git a/controller/console/reconciler_test.go b/controller/console/reconciler_test.go index 0e3162c..baabff2 100644 --- a/controller/console/reconciler_test.go +++ b/controller/console/reconciler_test.go @@ -69,8 +69,8 @@ func TestEnsureAgenticConsole_CreatesAllResources(t *testing.T) { if err := fc.Get(context.Background(), types.NamespacedName{Name: pluginName, Namespace: cfg.Namespace}, &dep); err != nil { t.Errorf("Deployment not created: %v", err) } - if dep.Spec.Template.Spec.Containers[0].Image != cfg.Image { - t.Errorf("Deployment image = %q, want %q", dep.Spec.Template.Spec.Containers[0].Image, cfg.Image) + if dep.Spec.Template.Spec.Containers[0].Image != "quay.io/test/agentic-console:latest" { + t.Errorf("Deployment image = %q, want %q", dep.Spec.Template.Spec.Containers[0].Image, "quay.io/test/agentic-console:latest") } var plugin consolev1.ConsolePlugin diff --git a/controller/proposal/handlers.go b/controller/proposal/handlers.go index 8754298..913f630 100644 --- a/controller/proposal/handlers.go +++ b/controller/proposal/handlers.go @@ -62,6 +62,9 @@ func (r *ProposalReconciler) handleAnalysis( if err != nil { return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, err) } + if !analysisResult.Success { + return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, fmt.Errorf("analysis agent reported failure")) + } base = proposal.DeepCopy() completedAt := metav1.Now() startTime := conditionTime(proposal.Status.Conditions, agenticv1alpha1.ProposalConditionAnalyzed) @@ -125,6 +128,9 @@ func (r *ProposalReconciler) handleRevision( if err != nil { return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, err) } + if !analysisResult.Success { + return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, fmt.Errorf("analysis agent reported failure")) + } base = proposal.DeepCopy() completedAt := metav1.Now() diff --git a/controller/proposal/reconciler.go b/controller/proposal/reconciler.go index 16f1d2d..7686add 100644 --- a/controller/proposal/reconciler.go +++ b/controller/proposal/reconciler.go @@ -163,10 +163,10 @@ func (r *ProposalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // SetupWithManager sets up the controller with the Manager. func (r *ProposalReconciler) SetupWithManager(mgr ctrl.Manager) error { maxConcurrent := int(agenticv1alpha1.DefaultMaxConcurrentProposals) - var policy agenticv1alpha1.ApprovalPolicy - if err := mgr.GetAPIReader().Get(context.Background(), client.ObjectKey{Name: "cluster"}, &policy); err == nil { - if policy.Spec.MaxConcurrentProposals > 0 { - maxConcurrent = int(policy.Spec.MaxConcurrentProposals) + var ap agenticv1alpha1.ApprovalPolicy + if err := mgr.GetAPIReader().Get(context.Background(), client.ObjectKey{Name: "cluster"}, &ap); err == nil { + if ap.Spec.MaxConcurrentProposals > 0 { + maxConcurrent = int(ap.Spec.MaxConcurrentProposals) } } return ctrl.NewControllerManagedBy(mgr). diff --git a/controller/proposal/reconciler_test.go b/controller/proposal/reconciler_test.go index 1bafdfb..4926563 100644 --- a/controller/proposal/reconciler_test.go +++ b/controller/proposal/reconciler_test.go @@ -210,7 +210,7 @@ func fakeBaseTemplate() *unstructured.Unstructured { "apiVersion": "extensions.agents.x-k8s.io/v1alpha1", "kind": "SandboxTemplate", "metadata": map[string]any{ - "name": "test-template", + "name": defaultBaseTemplateName, "namespace": "test-ns", }, "spec": map[string]any{ @@ -253,9 +253,8 @@ func newMockSandboxAgent(analysisJSON, executionJSON, verificationJSON string) ( httpClient.response = &agentRunResponse{Response: json.RawMessage(resp)} return httpClient }, - Namespace: "test-ns", - BaseTemplateName: "test-template", - Timeout: 5 * time.Minute, + Namespace: "test-ns", + Timeout: 5 * time.Minute, } return caller, sandbox } diff --git a/controller/proposal/sandbox_agent.go b/controller/proposal/sandbox_agent.go index 7fb967a..5d73c5d 100644 --- a/controller/proposal/sandbox_agent.go +++ b/controller/proposal/sandbox_agent.go @@ -13,7 +13,10 @@ import ( agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" ) -const defaultSandboxTimeout = 5 * time.Minute +const ( + defaultSandboxTimeout = 5 * time.Minute + defaultBaseTemplateName = "lightspeed-agent" +) type analysisResponse struct { Success bool `json:"success"` @@ -35,27 +38,25 @@ type verificationResponse struct { // SandboxAgentCaller implements AgentCaller by claiming a sandbox pod, // calling the agent HTTP service, and releasing the sandbox on completion. type SandboxAgentCaller struct { - Sandbox SandboxProvider - K8sClient client.Client - ClientFactory func(endpoint string) AgentHTTPClientInterface - Namespace string - BaseTemplateName string - Timeout time.Duration + Sandbox SandboxProvider + K8sClient client.Client + ClientFactory func(endpoint string) AgentHTTPClientInterface + Namespace string + Timeout time.Duration } func NewSandboxAgentCaller( sandbox SandboxProvider, k8sClient client.Client, clientFactory func(endpoint string) AgentHTTPClientInterface, - namespace, baseTemplateName string, + namespace string, ) *SandboxAgentCaller { return &SandboxAgentCaller{ - Sandbox: sandbox, - K8sClient: k8sClient, - ClientFactory: clientFactory, - Namespace: namespace, - BaseTemplateName: baseTemplateName, - Timeout: defaultSandboxTimeout, + Sandbox: sandbox, + K8sClient: k8sClient, + ClientFactory: clientFactory, + Namespace: namespace, + Timeout: defaultSandboxTimeout, } } @@ -164,7 +165,7 @@ func (s *SandboxAgentCaller) callWithSandbox( query string, agentCtx *agentContext, ) (json.RawMessage, error) { - templateName, err := EnsureAgentTemplate(ctx, s.K8sClient, s.BaseTemplateName, s.Namespace, stepName, step.Agent, step.LLM, step.Tools) + templateName, err := EnsureAgentTemplate(ctx, s.K8sClient, defaultBaseTemplateName, s.Namespace, stepName, step.Agent, step.LLM, step.Tools) if err != nil { return nil, fmt.Errorf("ensure agent template: %w", err) } diff --git a/controller/proposal/sandbox_agent_test.go b/controller/proposal/sandbox_agent_test.go index aa06574..3643734 100644 --- a/controller/proposal/sandbox_agent_test.go +++ b/controller/proposal/sandbox_agent_test.go @@ -62,7 +62,6 @@ func newTestSandboxAgentCaller(sandbox *mockSandboxProvider, httpClient *mockHTT K8sClient: fc, ClientFactory: func(_ string) AgentHTTPClientInterface { return httpClient }, Namespace: "test-ns", - BaseTemplateName: "test-template", Timeout: 5 * time.Minute, } } @@ -78,7 +77,6 @@ func newTestSandboxAgentCallerWithProposal(sandbox *mockSandboxProvider, httpCli K8sClient: fc, ClientFactory: func(_ string) AgentHTTPClientInterface { return httpClient }, Namespace: "test-ns", - BaseTemplateName: "test-template", Timeout: 5 * time.Minute, } } diff --git a/controller/setup.go b/controller/setup.go new file mode 100644 index 0000000..3c30161 --- /dev/null +++ b/controller/setup.go @@ -0,0 +1,50 @@ +package controller + +import ( + "context" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/manager" + + agenticconsole "github.com/openshift/lightspeed-agentic-operator/controller/console" + "github.com/openshift/lightspeed-agentic-operator/controller/proposal" +) + +type Options struct { + Namespace string + AgenticConsoleImage string + AgenticSandboxImage string +} + +func Setup(mgr ctrl.Manager, opts Options) error { + log := ctrl.Log.WithName("agentic-setup") + + sandboxMgr := proposal.NewSandboxManager(mgr.GetClient(), opts.Namespace) + agentCaller := proposal.NewSandboxAgentCaller( + sandboxMgr, + mgr.GetClient(), + proposal.NewAgentHTTPClient, + opts.Namespace, + ) + + if err := (&proposal.ProposalReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Proposal"), + Agent: agentCaller, + }).SetupWithManager(mgr); err != nil { + return err + } + log.Info("Proposal controller registered") + + if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { + return agenticconsole.EnsureAgenticConsole(ctx, mgr.GetClient(), agenticconsole.AgenticConsoleConfig{ + Image: opts.AgenticConsoleImage, + Namespace: opts.Namespace, + }) + })); err != nil { + return err + } + log.Info("Agentic console runnable registered") + + return nil +} diff --git a/examples/setup/02-approval-policy.yaml b/examples/setup/02-approval-policy.yaml index af04fb3..391c864 100644 --- a/examples/setup/02-approval-policy.yaml +++ b/examples/setup/02-approval-policy.yaml @@ -1,11 +1,7 @@ -# ApprovalPolicy — cluster-scoped singleton that configures default approval -# behavior for proposal workflow steps. +# ApprovalPolicy — cluster-scoped singleton that configures default +# approval behavior for proposal workflow steps. # -# Each step has a built-in approval gate. The step does not run until approval -# is present — either auto-approved via this policy or explicitly approved by -# the user on the ProposalApproval resource. -# -# If no ApprovalPolicy exists, all steps default to Manual. +# If no ApprovalPolicy exists, all approval steps default to Manual. --- apiVersion: agentic.openshift.io/v1alpha1 kind: ApprovalPolicy @@ -13,6 +9,7 @@ metadata: name: cluster spec: maxAttempts: 3 + maxConcurrentProposals: 5 stages: - name: Analysis approval: Manual diff --git a/examples/setup/03-proposals.yaml b/examples/setup/03-proposals.yaml index 81d267f..04fa741 100644 --- a/examples/setup/03-proposals.yaml +++ b/examples/setup/03-proposals.yaml @@ -12,7 +12,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: github-token description: "GitHub API token for repository access" @@ -39,7 +42,10 @@ spec: - staging tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart verification: @@ -57,7 +63,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart --- @@ -73,7 +82,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart --- @@ -89,7 +101,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: github-token mountAs: diff --git a/examples/setup/04-acs-proposals.yaml b/examples/setup/04-acs-proposals.yaml index 049e51a..6c8eb20 100644 --- a/examples/setup/04-acs-proposals.yaml +++ b/examples/setup/04-acs-proposals.yaml @@ -29,7 +29,10 @@ spec: - staging tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: acs-api-token description: "ACS Central API token for querying violations" @@ -63,7 +66,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: acs-api-token description: "ACS Central API token for querying violations" @@ -94,7 +100,10 @@ spec: - staging tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: acs-api-token mountAs: @@ -127,7 +136,8 @@ spec: agent: smart tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest paths: [/skills/acs-remediation, /skills/acs-compliance] execution: agent: default @@ -135,5 +145,6 @@ spec: agent: fast tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest paths: [/skills/acs-compliance] diff --git a/examples/setup/05-alertmanager-proposals.yaml b/examples/setup/05-alertmanager-proposals.yaml index bcca9d1..07be00b 100644 --- a/examples/setup/05-alertmanager-proposals.yaml +++ b/examples/setup/05-alertmanager-proposals.yaml @@ -24,7 +24,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart execution: @@ -49,7 +52,10 @@ spec: - openshift-lightspeed tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart diff --git a/examples/setup/06-ossm-proposals.yaml b/examples/setup/06-ossm-proposals.yaml index a309733..e0928b3 100644 --- a/examples/setup/06-ossm-proposals.yaml +++ b/examples/setup/06-ossm-proposals.yaml @@ -26,7 +26,10 @@ spec: - istio-system tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart execution: @@ -52,6 +55,9 @@ spec: - bookinfo tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart diff --git a/examples/setup/07-assisted-proposals.yaml b/examples/setup/07-assisted-proposals.yaml index d24054d..3c68d1f 100644 --- a/examples/setup/07-assisted-proposals.yaml +++ b/examples/setup/07-assisted-proposals.yaml @@ -22,7 +22,10 @@ spec: - production tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token requiredSecrets: - name: github-token description: "GitHub API token for repository access" @@ -49,6 +52,9 @@ spec: - staging tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart diff --git a/examples/setup/08-mcp-demo.yaml b/examples/setup/08-mcp-demo.yaml index 740ddb1..d9f7750 100644 --- a/examples/setup/08-mcp-demo.yaml +++ b/examples/setup/08-mcp-demo.yaml @@ -26,6 +26,9 @@ spec: - openshift-lightspeed tools: skills: - - image: image-registry.openshift-image-registry.svc:5000/openshift-lightspeed/lightspeed-skills:latest + - # TODO: Replace with Konflux-built image when available for https://github.com/openshift/agentic-skills + image: quay.io/harpatil/agentic-skills:latest + paths: + - /skills/find-token analysis: agent: smart