Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/v1alpha1/proposal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand All @@ -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:
Expand Down
24 changes: 8 additions & 16 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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").
Expand All @@ -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
Expand All @@ -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"`
}
6 changes: 3 additions & 3 deletions api/v1alpha1/tools_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 19 additions & 25 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -13,28 +15,32 @@ 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()

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)))
Expand All @@ -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)
}

Expand All @@ -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)
Expand Down
Loading