From d21f2bb47fe92f4fafef4c61808c20fca6ea0a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 3 Feb 2026 21:13:11 +0100 Subject: [PATCH 1/9] docs(spec): add v1alpha2 operator-generated config specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This specification defines the v1alpha2 API for the LlamaStackDistribution CRD, enabling operator-generated server configuration. Key features: - New spec fields: providers, resources, storage, networking, workload - Config generation from high-level abstracted spec - Base config extraction from OCI image labels - Polymorphic field support (single object or list) - Backward compatibility via conversion webhook - Integration with spec 001 external providers Includes implementation plan (5 phases) and detailed task breakdown (33 tasks with dependencies). Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../alternatives/init-container-extraction.md | 2 +- specs/002-operator-generated-config/plan.md | 299 +++------------ specs/002-operator-generated-config/spec.md | 255 +++---------- specs/002-operator-generated-config/tasks.md | 351 ++++-------------- 4 files changed, 176 insertions(+), 731 deletions(-) diff --git a/specs/002-operator-generated-config/alternatives/init-container-extraction.md b/specs/002-operator-generated-config/alternatives/init-container-extraction.md index d23a52f42..770d974e4 100644 --- a/specs/002-operator-generated-config/alternatives/init-container-extraction.md +++ b/specs/002-operator-generated-config/alternatives/init-container-extraction.md @@ -96,7 +96,7 @@ func (c *BaseConfigCache) GetOrCreate(ctx context.Context, name, image string) ( } // Verify image matches (invalidate cache if distribution changed) - if cm.Annotations["llamastack.io/source-image"] != image { + if cm.Annotations["llamastack.ai/source-image"] != image { // Image changed, delete cached config to trigger re-extraction if err := c.client.Delete(ctx, &cm); err != nil { return nil, err diff --git a/specs/002-operator-generated-config/plan.md b/specs/002-operator-generated-config/plan.md index e80fae684..7ea5eff8f 100644 --- a/specs/002-operator-generated-config/plan.md +++ b/specs/002-operator-generated-config/plan.md @@ -1,105 +1,12 @@ # Implementation Plan: Operator-Generated Server Configuration (v1alpha2) -**Branch**: `002-operator-generated-config` | **Date**: 2026-02-02 | **Spec**: [spec.md](spec.md) +**Spec**: 002-operator-generated-config +**Created**: 2026-02-02 **Status**: Ready for Implementation -## Summary - -Introduce a v1alpha2 API version for the LlamaStackDistribution CRD that enables the operator to generate server configuration (config.yaml) from a high-level, abstracted CR specification. Users configure providers, resources, and storage with minimal YAML while the operator handles config generation, secret resolution, and atomic Deployment updates. - -## Technical Context - -**Language/Version**: Go 1.25 (go.mod) -**Primary Dependencies**: controller-runtime v0.22.4, kubebuilder, kustomize/api v0.21.0, client-go v0.34.3, go-containerregistry v0.20.7 -**Storage**: Kubernetes ConfigMaps (generated), Secrets (referenced via secretKeyRef) -**Testing**: Go test, envtest (controller-runtime), testify v1.11.1 -**Target Platform**: Kubernetes 1.30+ -**Project Type**: Kubernetes operator (single binary) -**Performance Goals**: Config generation < 5 seconds (NFR-002) -**Constraints**: Namespace-scoped RBAC (constitution ยง1.1), air-gapped registry support, deterministic output (NFR-001) -**Scale/Scope**: Single CRD with 2 API versions, ~8 new Go packages - -## Constitution Check - -*GATE: PASS (1 documented deviation, 0 unresolved violations)* - -| # | Principle | Status | Notes | -|---|-----------|--------|-------| -| ยง1.1 Namespace-Scoped | DEVIATION | ValidatingWebhookConfiguration is cluster-scoped (standard operator pattern). Documented in spec.md Security Considerations. | -| ยง1.2 Idempotent Reconciliation | PASS | Deterministic config generation (NFR-001). Hash-based change detection. | -| ยง1.3 Owner References | PASS | FR-025 requires owner refs on generated ConfigMaps. | -| ยง2.1 Kubebuilder Validation | PASS | CEL (FR-070-072), webhook (FR-076-078), kubebuilder tags. | -| ยง2.2 Optional Fields | PASS | Pointer types for optional structs throughout. | -| ยง2.3 Defaults | PASS | Constants for DefaultServerPort, storage type defaults. | -| ยง2.4 Status Subresource | PASS | New conditions: ConfigGenerated, DeploymentUpdated, Available, SecretsResolved. | -| ยง3.2 Conditions | PASS | Standard metav1.Condition with defined constants for types, reasons, messages. | -| ยง4.1 Error Wrapping | PASS | All errors wrapped with %w and context. | -| ยง6.1 Table-Driven Tests | PASS | Test plan follows constitution patterns. | -| ยง6.4 Builder Pattern | PASS | Existing test builders extended for v1alpha2. | -| ยง13.2 AI Attribution | PASS | Assisted-by format (no Co-Authored-By). | - -## Project Structure - -### Documentation (this feature) - -```text -specs/002-operator-generated-config/ -โ”œโ”€โ”€ spec.md # Feature specification -โ”œโ”€โ”€ plan.md # This file -โ”œโ”€โ”€ research.md # Phase 0 research decisions -โ”œโ”€โ”€ data-model.md # Entity definitions and relationships -โ”œโ”€โ”€ quickstart.md # Usage examples -โ”œโ”€โ”€ contracts/ # Interface contracts -โ”‚ โ”œโ”€โ”€ crd-schema.yaml -โ”‚ โ”œโ”€โ”€ config-generation.yaml -โ”‚ โ””โ”€โ”€ status-conditions.yaml -โ”œโ”€โ”€ tasks.md # Implementation tasks -โ”œโ”€โ”€ review_summary.md # Executive brief -โ””โ”€โ”€ alternatives/ # Alternative approaches evaluated -``` +## Overview -### Source Code (repository root) - -```text -api/ -โ”œโ”€โ”€ v1alpha1/ # Existing types + conversion spoke -โ”‚ โ”œโ”€โ”€ llamastackdistribution_types.go -โ”‚ โ””โ”€โ”€ llamastackdistribution_conversion.go # New: v1alpha1 spoke -โ””โ”€โ”€ v1alpha2/ # New API version - โ”œโ”€โ”€ groupversion_info.go - โ”œโ”€โ”€ llamastackdistribution_types.go - โ”œโ”€โ”€ llamastackdistribution_webhook.go # Validating webhook - โ”œโ”€โ”€ llamastackdistribution_conversion.go # Hub (no-op) - โ””โ”€โ”€ zz_generated.deepcopy.go # Generated - -pkg/config/ # New: config generation engine -โ”œโ”€โ”€ config.go # Main orchestration -โ”œโ”€โ”€ generator.go # YAML generation -โ”œโ”€โ”€ resolver.go # Base config resolution -โ”œโ”€โ”€ provider.go # Provider expansion -โ”œโ”€โ”€ resource.go # Resource expansion -โ”œโ”€โ”€ storage.go # Storage configuration -โ”œโ”€โ”€ secret_resolver.go # Secret reference resolution -โ”œโ”€โ”€ version.go # Config schema version handling -โ”œโ”€โ”€ types.go # Internal config types -โ””โ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction - -configs/ # New: embedded default configs -โ”œโ”€โ”€ starter/config.yaml -โ”œโ”€โ”€ remote-vllm/config.yaml -โ”œโ”€โ”€ meta-reference-gpu/config.yaml -โ””โ”€โ”€ postgres-demo/config.yaml - -controllers/ # Extended for v1alpha2 -โ”œโ”€โ”€ llamastackdistribution_controller.go # Updated reconciliation -โ””โ”€โ”€ status.go # New conditions - -config/webhook/ # New: webhook kustomize config -โ””โ”€โ”€ manifests.yaml - -tests/e2e/ # Extended -โ””โ”€โ”€ config_generation_test.go # New: v1alpha2 e2e tests -``` +This plan outlines the implementation strategy for the Operator-Generated Server Configuration feature, introducing the v1alpha2 API version with config generation capabilities. ## Implementation Phases @@ -153,6 +60,7 @@ type ProviderConfig struct { Provider string `json:"provider"` Endpoint string `json:"endpoint,omitempty"` ApiKey *SecretKeyRef `json:"apiKey,omitempty"` + Host *SecretKeyRef `json:"host,omitempty"` Settings map[string]interface{} `json:"settings,omitempty"` } ``` @@ -248,7 +156,6 @@ type WorkloadOverrides struct { // +kubebuilder:validation:XValidation:rule="!(has(self.providers) && has(self.overrideConfig))",message="providers and overrideConfig are mutually exclusive" // +kubebuilder:validation:XValidation:rule="!(has(self.resources) && has(self.overrideConfig))",message="resources and overrideConfig are mutually exclusive" // +kubebuilder:validation:XValidation:rule="!(has(self.storage) && has(self.overrideConfig))",message="storage and overrideConfig are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.disabled) && has(self.overrideConfig))",message="disabled and overrideConfig are mutually exclusive" ``` #### 1.8 Generate CRD Manifests @@ -290,8 +197,7 @@ make manifests pkg/config/ โ”œโ”€โ”€ config.go # Main orchestration โ”œโ”€โ”€ generator.go # YAML generation -โ”œโ”€โ”€ resolver.go # Base config resolution (embedded + OCI) -โ”œโ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction +โ”œโ”€โ”€ extractor.go # Base config extraction from images โ”œโ”€โ”€ provider.go # Provider expansion โ”œโ”€โ”€ resource.go # Resource expansion โ”œโ”€โ”€ storage.go # Storage configuration @@ -300,143 +206,18 @@ pkg/config/ โ””โ”€โ”€ types.go # Internal config types ``` -#### 2.2 Implement Base Config Resolution (Phased) - -**Files**: -- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic -- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) -- `pkg/config/oci_extractor.go` - Phase 2: OCI label extraction - -**Approach**: Base config resolution follows a phased strategy. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`, requiring no changes to distribution image builds. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override when distribution images support it. - -> **Alternative**: An init container approach is documented in `alternatives/init-container-extraction.md` for cases where neither embedded configs nor OCI labels are available. - -**Resolution Priority**: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 1. (Phase 2) Check OCI labels on resolved image โ”‚ -โ”‚ โ””โ”€โ”€ If present: extract config from labels โ”‚ -โ”‚ (takes precedence over embedded) โ”‚ -โ”‚ โ”‚ -โ”‚ 2. (Phase 1) Check embedded configs for distribution.name โ”‚ -โ”‚ โ””โ”€โ”€ If found: use go:embed config for that distribution โ”‚ -โ”‚ โ”‚ -โ”‚ 3. No config available: โ”‚ -โ”‚ โ”œโ”€โ”€ distribution.name โ†’ should not happen (build error) โ”‚ -โ”‚ โ””โ”€โ”€ distribution.image โ†’ require overrideConfig โ”‚ -โ”‚ (set ConfigGenerated=False, reason BaseConfigReq.) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -##### Phase 1: Embedded Default Configs (MVP) +#### 2.2 Implement Base Config Extraction (OCI Label Approach) -The operator binary embeds default configs for all named distributions via `go:embed`: +**File**: `pkg/config/extractor.go` -```go -// pkg/config/resolver.go - -import "embed" +**Approach**: Extract the distribution's base `config.yaml` from OCI image labels, using the `k8schain` authenticator for registry access. This enables single-phase reconciliation and works with imagePullSecrets in air-gapped environments. -//go:embed configs -var embeddedConfigs embed.FS - -type BaseConfigResolver struct { - distributionImages map[string]string // from distributions.json - imageOverrides map[string]string // from operator ConfigMap - ociExtractor *ImageConfigExtractor // nil in Phase 1 -} - -func NewBaseConfigResolver(distImages, overrides map[string]string) *BaseConfigResolver { - return &BaseConfigResolver{ - distributionImages: distImages, - imageOverrides: overrides, - } -} - -func (r *BaseConfigResolver) Resolve(ctx context.Context, dist DistributionSpec) (*BaseConfig, string, error) { - // Resolve distribution to concrete image reference - image, err := r.resolveImage(dist) - if err != nil { - return nil, "", err - } - - // Phase 2: Check OCI labels first (when ociExtractor is configured) - if r.ociExtractor != nil { - config, err := r.ociExtractor.Extract(ctx, image) - if err == nil { - return config, image, nil - } - // Fall through to embedded if OCI extraction fails - log.FromContext(ctx).V(1).Info("OCI config extraction failed, falling back to embedded", - "image", image, "error", err) - } - - // Phase 1: Use embedded config for named distributions - if dist.Name != "" { - config, err := r.loadEmbeddedConfig(dist.Name) - if err != nil { - return nil, "", fmt.Errorf("failed to load embedded config for distribution %q: %w", dist.Name, err) - } - return config, image, nil - } - - // distribution.image without OCI labels or embedded config - return nil, "", fmt.Errorf("direct image references require either overrideConfig.configMapName or OCI config labels on the image") -} - -func (r *BaseConfigResolver) loadEmbeddedConfig(name string) (*BaseConfig, error) { - data, err := embeddedConfigs.ReadFile(fmt.Sprintf("configs/%s/config.yaml", name)) - if err != nil { - return nil, fmt.Errorf("no embedded config for distribution %q: %w", name, err) - } - - var config BaseConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("invalid embedded config for distribution %q: %w", name, err) - } - - return &config, nil -} - -func (r *BaseConfigResolver) resolveImage(dist DistributionSpec) (string, error) { - if dist.Image != "" { - return dist.Image, nil - } - - // Check image-overrides first (downstream builds) - if override, ok := r.imageOverrides[dist.Name]; ok { - return override, nil - } - - // Look up in distributions.json - if image, ok := r.distributionImages[dist.Name]; ok { - return image, nil - } - - return "", fmt.Errorf("unknown distribution name %q: not found in distributions.json", dist.Name) -} -``` - -**Embedded config directory** (created at build time): -``` -configs/ -โ”œโ”€โ”€ starter/config.yaml -โ”œโ”€โ”€ remote-vllm/config.yaml -โ”œโ”€โ”€ meta-reference-gpu/config.yaml -โ””โ”€โ”€ postgres-demo/config.yaml -``` - -**Air-gapped support**: Embedded configs work regardless of registry access. The `image-overrides` mechanism allows downstream builds (e.g., RHOAI) to remap distribution names to internal registry images without rebuilding the operator. - -##### Phase 2: OCI Label Extraction (Enhancement) - -**File**: `pkg/config/oci_extractor.go` - -When distribution images include OCI config labels, the extracted config takes precedence over embedded defaults. This enables `distribution.image` usage without `overrideConfig`. +> **Alternative**: An init container approach is documented in `alternatives/init-container-extraction.md` for cases where OCI labels are not available. **OCI Label Convention**: +Distribution images embed config.yaml in OCI labels using a tiered strategy: + | Label | Purpose | When Used | |-------|---------|-----------| | `io.llamastack.config.base64` | Base64-encoded config.yaml | Small configs (< 50KB) | @@ -444,6 +225,22 @@ When distribution images include OCI config labels, the extracted config takes p | `io.llamastack.config.path` | Path within the layer | Used with layer reference | | `io.llamastack.config.version` | Config schema version | Always | +**Extraction Priority**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. Check io.llamastack.config.base64 โ”‚ +โ”‚ โ””โ”€โ”€ If present: decode and return (~10KB manifest fetch)โ”‚ +โ”‚ โ”‚ +โ”‚ 2. Check io.llamastack.config.layer + .path โ”‚ +โ”‚ โ””โ”€โ”€ If present: fetch specific layer, extract file โ”‚ +โ”‚ (~10-100KB single layer fetch) โ”‚ +โ”‚ โ”‚ +โ”‚ 3. No labels: Return error with guidance โ”‚ +โ”‚ โ””โ”€โ”€ Distribution image must include config labels โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + **Registry Authentication**: Uses `k8schain` from `go-containerregistry` to authenticate the same way kubelet does: @@ -464,7 +261,7 @@ import ( **Implementation**: ```go -// pkg/config/oci_extractor.go +// pkg/config/extractor.go type ConfigLocation struct { Base64 string // Inline base64 encoded config @@ -511,7 +308,7 @@ func (e *ImageConfigExtractor) Extract(ctx context.Context, imageRef string) (*B } // Fetch config location from image labels - loc, err := e.getConfigLocation(imageRef, keychain) + loc, err := e.getConfigLocation(ctx, imageRef, keychain) if err != nil { return nil, err } @@ -541,7 +338,7 @@ func (e *ImageConfigExtractor) Extract(ctx context.Context, imageRef string) (*B return config, nil } -func (e *ImageConfigExtractor) getConfigLocation(imageRef string, kc authn.Keychain) (*ConfigLocation, error) { +func (e *ImageConfigExtractor) getConfigLocation(ctx context.Context, imageRef string, kc authn.Keychain) (*ConfigLocation, error) { configJSON, err := crane.Config(imageRef, crane.WithAuthFromKeychain(kc)) if err != nil { return nil, fmt.Errorf("failed to fetch image config: %w", err) @@ -630,9 +427,9 @@ func (e *ImageConfigExtractor) extractFromLayer( } ``` -**Distribution Image Build Integration** (Phase 2): +**Distribution Image Build Integration**: -Labels are added post-build using `crane mutate` (solves the chicken-and-egg problem where layer digests are only known after build): +Labels are added post-build using `crane mutate` (solves the chicken-and-egg problem): ```bash #!/bin/bash @@ -682,7 +479,7 @@ fi - Labels added after build, so layer digest is known - Works with any registry that supports OCI manifests -**Air-Gapped / OpenShift Support** (Phase 2): +**Air-Gapped / OpenShift Support**: The `k8schain` authenticator handles: - imagePullSecrets from ServiceAccount @@ -712,9 +509,9 @@ The `k8schain` authenticator handles: - In-memory caching by digest (fast for repeated reconciles) **Cons**: -- Phase 2 requires distribution images to include config labels +- Requires distribution images to include config labels - Requires `go-containerregistry` dependency -- Distribution build process must use `crane mutate` (Phase 2) +- Distribution build process must use `crane mutate` #### 2.3 Implement Provider Expansion @@ -933,9 +730,10 @@ func (r *Reconciler) ShouldExposeRoute(spec *v1alpha2.NetworkingSpec) bool { if spec.Expose.Enabled != nil { return *spec.Expose.Enabled } - // expose: {} (non-nil pointer, all zero-valued fields) is treated as - // expose: true per edge case "Polymorphic expose with empty object" - return true + if spec.Expose.Hostname != "" { + return true + } + return false } ``` @@ -1002,13 +800,18 @@ type ConfigGenerationStatus struct { **File**: `api/v1alpha2/llamastackdistribution_conversion.go` -**Approach**: v1alpha2 is the hub (storage version). In controller-runtime, the Hub type only implements a `Hub()` marker method. Conversion logic (`ConvertTo`/`ConvertFrom`) lives on the Spoke (v1alpha1). +**Approach**: v1alpha2 is the hub (storage version) ```go -// Hub marks v1alpha2 as the storage version for conversion. -// The Hub interface requires only this marker method. -// All conversion logic is implemented on the v1alpha1 spoke. -func (dst *LlamaStackDistribution) Hub() {} +func (src *LlamaStackDistribution) ConvertTo(dstRaw conversion.Hub) error { + // v1alpha2 is hub, this is a no-op + return nil +} + +func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { + // v1alpha2 is hub, this is a no-op + return nil +} ``` #### 4.2 Implement v1alpha1 Spoke Conversion @@ -1056,7 +859,7 @@ func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: llamastackdistributions.llamastack.io + name: llamastackdistributions.llamastack.ai spec: conversion: strategy: Webhook diff --git a/specs/002-operator-generated-config/spec.md b/specs/002-operator-generated-config/spec.md index f70d5c74f..62b3be220 100644 --- a/specs/002-operator-generated-config/spec.md +++ b/specs/002-operator-generated-config/spec.md @@ -110,21 +110,6 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat 2. **Given** a v1alpha1 CR, **When** I retrieve it as v1alpha2, **Then** the conversion webhook translates fields correctly 3. **Given** a v1alpha2 CR, **When** I retrieve it as v1alpha1, **Then** the conversion webhook translates fields correctly (where mappable) -### User Story 8 - Runtime Configuration Updates (Priority: P1) - -As a platform operator, I want to update the LLSD CR (e.g., add a provider, change storage) and have the running LlamaStack instance pick up the changes automatically, so that I can manage configuration declaratively without manual restarts. - -**Why this priority**: Day-2 operations are essential for production use. Without this, users must delete and recreate CRs to change configuration. - -**Independent Test**: Deploy a LLSD CR, wait for Ready, modify the CR's providers section, verify the Pod restarts with the updated config.yaml. - -**Acceptance Scenarios**: - -1. **Given** a running LLSD instance, **When** I add a new provider to `spec.providers`, **Then** the operator regenerates config.yaml, creates a new ConfigMap, and triggers a rolling update -2. **Given** a running LLSD instance, **When** I modify `spec.providers` but the generated config.yaml is identical (e.g., whitespace-only change), **Then** the operator does NOT restart the Pod -3. **Given** a running LLSD instance, **When** I update `spec.providers` with an invalid configuration, **Then** the operator preserves the current running config, reports the error in status, and does NOT disrupt the running instance -4. **Given** a running LLSD instance, **When** I change `spec.distribution.name` to a different distribution, **Then** the operator updates both the image and config atomically in a single Deployment update - ### Edge Cases - **Provider with settings escape hatch**: @@ -151,30 +136,6 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - What: Distribution image has unsupported config.yaml version - Expected: Reconciliation fails with clear error about version incompatibility -- **CR update during active rollout**: - - What: User updates the CR while a previous config change is still rolling out - - Expected: Operator generates config from the latest CR spec, superseding the in-progress rollout. The Deployment converges to the newest desired state. - -- **Operator upgrade with running LLSD instances**: - - What: Operator is upgraded from v1 to v2, changing the image that `distribution.name: rh-dev` resolves to - - Expected: Operator detects the image change, regenerates config using the new base config matching the new image, and updates the Deployment atomically (new image + new config together). If config generation fails for the new image, the operator preserves the current running Deployment and reports the error in status. - -- **Config generation failure on update**: - - What: User changes CR in a way that produces an invalid merged config (e.g., references a provider type not supported by the distribution) - - Expected: Operator keeps the current running config and Deployment unchanged, sets `ConfigGenerated=False` with a descriptive error, and does not trigger a Pod restart - -- **Deeply nested secretKeyRef in settings**: - - What: User specifies `settings: {database: {connection: {secretKeyRef: {name: db, key: url}}}}` - - Expected: The nested secretKeyRef is NOT resolved as a secret reference. Only top-level settings values are inspected for secretKeyRef (e.g., `settings.host.secretKeyRef`). The deeply nested object is passed through to config.yaml as a literal map. - -- **Tools specified without toolRuntime provider**: - - What: User specifies `resources.tools: [websearch]` but does not configure `providers.toolRuntime` - - Expected: If the base config provides a default toolRuntime provider, tools are registered with it. If no toolRuntime provider exists in either user config or base config, validation fails with: "resources.tools requires at least one toolRuntime provider to be configured" - -- **Shields specified without safety provider**: - - What: User specifies `resources.shields: [llama-guard]` but does not configure `providers.safety` - - Expected: If the base config provides a default safety provider, shields are registered with it. If no safety provider exists in either user config or base config, validation fails with: "resources.shields requires at least one safety provider to be configured" - ## Requirements ### Functional Requirements @@ -185,7 +146,7 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - **FR-002**: The `spec.distribution` field MUST support both `name` (mapped) and `image` (direct) forms, mutually exclusive - **FR-003**: The `spec.providers` section MUST support provider types: `inference`, `safety`, `vectorIo`, `toolRuntime`, `telemetry` - **FR-004**: Each provider MUST support polymorphic form: single object OR list of objects with explicit `id` field -- **FR-005**: Each provider MUST support fields: `provider` (type), `endpoint`, `apiKey` (secretKeyRef), `settings` (escape hatch). Provider-specific connection fields (e.g., `host` for vectorIo) MUST use `secretKeyRef` within `settings` rather than top-level named fields, keeping the provider schema uniform. The operator MUST recognize `secretKeyRef` objects only at the top level of `settings` values (i.e., `settings..secretKeyRef`), not at arbitrary nesting depth. Deeper nesting is passed through to config.yaml as-is without secret resolution. +- **FR-005**: Each provider MUST support fields: `provider` (type), `endpoint`, `apiKey` (secretKeyRef), `settings` (escape hatch) - **FR-006**: The `spec.resources` section MUST support: `models`, `tools`, `shields` - **FR-007**: Resources MUST support polymorphic form: simple string OR object with metadata - **FR-008**: The `spec.storage` section MUST have subsections: `kv` (key-value) and `sql` (relational) @@ -193,15 +154,12 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - **FR-010**: The `spec.networking` section MUST consolidate: `port`, `tls`, `expose`, `allowedFrom` - **FR-011**: The `networking.expose` field MUST support polymorphic form: boolean OR object with `hostname` - **FR-012**: The `spec.workload` section MUST contain K8s deployment settings: `replicas`, `workers`, `resources`, `autoscaling`, `storage`, `podDisruptionBudget`, `topologySpreadConstraints`, `overrides` -- **FR-013**: The `spec.overrideConfig` field MUST be mutually exclusive with `providers`, `resources`, `storage`, `disabled`. The referenced ConfigMap MUST reside in the same namespace as the LLSD CR (consistent with namespace-scoped RBAC, constitution section 1.1) +- **FR-013**: The `spec.overrideConfig` field MUST be mutually exclusive with `providers`, `resources`, `storage`, `disabled` - **FR-014**: The `spec.externalProviders` field MUST remain for integration with spec 001 #### Configuration Generation -- **FR-020**: The operator MUST resolve `distribution.name` to a concrete image reference using the embedded distribution registry (`distributions.json`) and operator config overrides (`image-overrides`) -- **FR-020a**: The operator MUST record the resolved image reference in `status.resolvedDistribution.image` after successful resolution -- **FR-020b**: When the resolved image changes between reconciliations (e.g., after operator upgrade changes the `distributions.json` mapping), the operator MUST regenerate config.yaml using the base config matching the new image and update the Deployment atomically (image + config in a single update) -- **FR-020c**: The operator MUST NOT update a running LLSD's config without also updating its image to match. Image and base config MUST always be consistent. +- **FR-020**: The operator MUST extract base config.yaml from the distribution container image - **FR-021**: The operator MUST generate a complete config.yaml by merging user configuration over base defaults - **FR-022**: The operator MUST resolve `secretKeyRef` references to environment variables with deterministic naming - **FR-023**: The operator MUST create a ConfigMap containing the generated config.yaml @@ -212,48 +170,36 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - **FR-028**: The operator MUST support config.yaml schema versions n and n-1 (current and previous) - **FR-029**: The operator MUST reject unsupported config.yaml versions with error: "Unsupported config.yaml version {version}. Supported versions: {list}" -#### Base Config Extraction (Phased Approach) +#### Base Config Extraction from OCI Images -The base config extraction follows a phased approach. Phase 1 provides an implementation that works without changes to distribution image build processes. Phase 2 adds OCI label-based extraction for distributions that support it. - -**Phase 1 - Embedded Default Configs (MVP)** - -- **FR-027a**: The operator MUST include embedded default configurations for all distribution names defined in `distributions.json`, shipped as part of the operator binary -- **FR-027b**: When `distribution.name` is specified, the operator MUST use the embedded config for that distribution as the base for config generation -- **FR-027c**: When `distribution.image` is specified (direct image reference, no named distribution), the operator MUST require `overrideConfig.configMapName` to provide the base configuration. If `overrideConfig` is not set and no OCI config labels are found (see Phase 2), the operator MUST set `ConfigGenerated=False` with reason `BaseConfigRequired` and message: "Direct image references require either overrideConfig.configMapName or OCI config labels on the image. See docs/configuration.md for details." -- **FR-027d**: The embedded configs MUST be versioned together with the distribution image mappings in `distributions.json`, ensuring each distribution name maps to a consistent (image, config) pair per operator release -- **FR-027e**: The operator MUST support air-gapped environments where images are mirrored to internal registries. The embedded config is used regardless of where the image is pulled from. - -**Phase 2 - OCI Label Extraction (Enhancement)** - -- **FR-027f**: Distribution images MAY include `config.yaml` in OCI labels using one of: +- **FR-027a**: Distribution images SHOULD include `config.yaml` in OCI labels using one of: - `io.llamastack.config.base64`: Base64-encoded config (for configs < 50KB) - `io.llamastack.config.layer` + `io.llamastack.config.path`: Layer digest and path reference (for larger configs) -- **FR-027g**: When OCI config labels are present on the resolved image, the operator MUST use the label-provided config as the base, taking precedence over embedded defaults -- **FR-027h**: The operator MUST use `k8schain` authentication to access registries using the same credentials as kubelet (imagePullSecrets, ServiceAccount) -- **FR-027i**: The operator MUST cache extracted configs by image digest to avoid repeated registry fetches -- **FR-027j**: OCI label extraction enables `distribution.image` usage without `overrideConfig`, since the image itself provides the base config +- **FR-027b**: The operator MUST extract base config from OCI labels in priority order: `base64` โ†’ `layer reference` +- **FR-027c**: The operator MUST use `k8schain` authentication to access registries using the same credentials as kubelet (imagePullSecrets, ServiceAccount) +- **FR-027d**: The operator MUST cache extracted configs by image digest to avoid repeated registry fetches +- **FR-027e**: When distribution image lacks config labels, the operator MUST: + 1. Set status condition `ConfigGenerated=False` with reason `MissingConfigLabels` + 2. Return error: "Distribution image {image} missing config labels. Add labels using `crane mutate` or use `overrideConfig` to provide configuration manually. See docs/configuration.md for details." +- **FR-027f**: The operator MUST support air-gapped environments where images are mirrored to internal registries +- **FR-027g**: When OCI labels are missing, users MAY use `overrideConfig.configMapName` as a workaround to provide full configuration manually #### Provider Configuration - **FR-030**: Provider `provider` field MUST map to `provider_type` with `remote::` prefix (e.g., `vllm` becomes `remote::vllm`) - **FR-031**: Provider `endpoint` field MUST map to `config.url` in config.yaml -- **FR-032**: Provider `apiKey.secretKeyRef` MUST be resolved to an environment variable and referenced as `${env.LLSD__}`, where `` is the provider's unique `id` (explicit or auto-generated per FR-035), uppercased with hyphens replaced by underscores. Example: provider ID `vllm-primary` with field `apiKey` produces `LLSD_VLLM_PRIMARY_API_KEY`. +- **FR-032**: Provider `apiKey.secretKeyRef` MUST be resolved to an environment variable and referenced as `${env.LLSD__API_KEY}` - **FR-033**: Provider `settings` MUST be merged into the provider's `config` section in config.yaml - **FR-034**: When multiple providers are specified, each MUST have an explicit `id` field - **FR-035**: Single provider without `id` MUST auto-generate `provider_id` from the `provider` field value -#### Telemetry Provider - -- **FR-036**: Telemetry providers follow the same schema as other provider types (FR-004, FR-005). The `provider` field maps to the telemetry backend (e.g., `opentelemetry`). The `endpoint` and `settings` fields configure the telemetry destination. No telemetry-specific fields are defined beyond the standard provider schema. - #### Resource Registration - **FR-040**: Simple model strings MUST be registered with the inference provider. When multiple inference providers are configured (list form), the first provider in list order is used. When a single provider is configured (object form), that provider is used. - **FR-041**: Model objects with explicit `provider` MUST be registered with the specified provider - **FR-042**: Model objects MAY include metadata fields: `contextLength`, `modelType`, `quantization` -- **FR-043**: Tools MUST be registered as tool groups with the configured toolRuntime provider. When no explicit toolRuntime provider is configured in the CR, the base config's toolRuntime provider is used. If no toolRuntime provider exists in either source, controller validation MUST fail with an actionable error -- **FR-044**: Shields MUST be registered with the configured safety provider. When no explicit safety provider is configured in the CR, the base config's safety provider is used. If no safety provider exists in either source, controller validation MUST fail with an actionable error +- **FR-043**: Tools MUST be registered as tool groups with default provider configuration +- **FR-044**: Shields MUST be registered with the configured safety provider #### Storage Configuration @@ -277,15 +223,12 @@ The base config extraction follows a phased approach. Phase 1 provides an implem #### Validation -- **FR-070**: CEL validation MUST enforce mutual exclusivity between `overrideConfig` and each of `providers`, `resources`, `storage`, and `disabled` +- **FR-070**: CEL validation MUST enforce mutual exclusivity between `providers` and `overrideConfig` - **FR-071**: CEL validation MUST require explicit `id` when multiple providers are specified for the same API - **FR-072**: CEL validation MUST enforce unique provider IDs across all provider types - **FR-073**: Controller validation MUST verify referenced Secrets exist before generating config - **FR-074**: Controller validation MUST verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` - **FR-075**: Validation errors MUST include actionable messages with field paths -- **FR-076**: A validating admission webhook MUST validate CR creation and update operations for constraints that cannot be expressed in CEL (e.g., Secret existence checks, ConfigMap existence for `overrideConfig`, cross-field semantic validation such as provider ID references in resources) -- **FR-077**: The validating webhook MUST return structured error responses with field paths and actionable messages following Kubernetes API conventions -- **FR-078**: The validating webhook MUST be deployed as part of the operator installation and configured via the operator's kustomize manifests with appropriate certificate management #### API Version Conversion @@ -300,19 +243,6 @@ The base config extraction follows a phased approach. Phase 1 provides an implem - **FR-091**: External providers (from spec 001) MUST be additive to inline providers - **FR-092**: When external provider ID conflicts with inline provider, external MUST override with warning -#### Runtime Configuration Updates - -- **FR-095**: When the CR `spec` changes, the operator MUST regenerate config.yaml, create a new ConfigMap with an updated content hash, and update the Deployment to reference the new ConfigMap -- **FR-096**: The operator MUST NOT restart Pods if the generated config.yaml content is identical to the currently deployed config (content hash comparison) -- **FR-097**: If config generation or validation fails during a CR update, the operator MUST preserve the current running Deployment (image, ConfigMap, env vars) unchanged and set status condition `ConfigGenerated=False` with the failure reason. The running instance MUST NOT be disrupted. -- **FR-098**: When `spec.distribution` changes (name or image), the operator MUST update the Deployment atomically per FR-100 -- **FR-099**: Status conditions MUST reflect the reconciliation state during updates: `ConfigGenerated` (config.yaml created successfully), `DeploymentUpdated` (Deployment spec updated), `Available` (at least one Pod is ready with the current config) - -#### Distribution Lifecycle - -- **FR-100**: The operator MUST update the Deployment's container image and its generated config atomically in a single Deployment update. There MUST be no intermediate state where the running image and config are mismatched. -- **FR-101**: When config generation fails after an operator upgrade (e.g., the new base config is incompatible with the user's CR fields), the operator MUST preserve the running Deployment per FR-097 and report the failure with reason `UpgradeConfigFailure` - ### Non-Functional Requirements - **NFR-001**: Configuration generation MUST be deterministic (same inputs produce same outputs) @@ -324,20 +254,9 @@ The base config extraction follows a phased approach. Phase 1 provides an implem ### External Dependencies -#### Operator Build Requirements (Phase 1) - -The operator binary MUST embed default configs for all named distributions: - -| Artifact | Description | Required | -|----------|-------------|----------| -| `distributions.json` | Maps distribution names to image references | Yes | -| `configs//config.yaml` | Default config.yaml for each named distribution | Yes | - -The `distributions.json` and corresponding config files are maintained together and updated as part of the operator release process. For downstream builds (e.g., RHOAI), the `image-overrides` mechanism in the operator ConfigMap allows overriding the embedded image references without rebuilding the operator. +#### Distribution Image Build Requirements -#### Distribution Image Build Requirements (Phase 2) - -Distribution images MAY include OCI labels for base config extraction. These labels are added post-build using `crane mutate`: +Distribution images must include OCI labels for base config extraction. These labels are added post-build using `crane mutate`: | Label | Description | Required | |-------|-------------|----------| @@ -360,8 +279,6 @@ crane mutate ${IMAGE}:build \ **Why post-build**: Labels containing layer digests cannot be added during build (the layer digest is only known after build). `crane mutate` solves this by updating only the config blob without modifying layers. -**Adoption path**: OCI labels are optional in Phase 1. Distributions that adopt labels gain the ability to be used with `distribution.image` without `overrideConfig`. A future "LLS Distribution Image Specification" document will formalize the label contract. - ### Key Entities - **ProviderSpec**: Configuration for a single provider (inference, safety, vectorIo, etc.) @@ -370,14 +287,13 @@ crane mutate ${IMAGE}:build \ - **NetworkingSpec**: Configuration for network exposure (port, TLS, expose, allowedFrom) - **WorkloadSpec**: Kubernetes deployment settings (replicas, resources, autoscaling) - **ExposeConfig**: Polymorphic expose configuration (bool or object with hostname) -- **ResolvedDistributionStatus**: Tracks the resolved image reference, config source (embedded/oci-label), and config hash for change detection ## CRD Schema ### Complete v1alpha2 Spec Structure ```yaml -apiVersion: llamastack.io/v1alpha2 +apiVersion: llamastack.ai/v1alpha2 kind: LlamaStackDistribution metadata: name: my-stack @@ -387,20 +303,18 @@ spec: providers: inference: - - id: vllm-primary - provider: vllm - endpoint: "http://vllm:8000" - apiKey: - secretKeyRef: {name: vllm-creds, key: token} - settings: - max_tokens: 8192 + provider: vllm + endpoint: "http://vllm:8000" + apiKey: + secretKeyRef: {name: vllm-creds, key: token} + settings: + max_tokens: 8192 safety: provider: llama-guard vectorIo: provider: pgvector - settings: - host: - secretKeyRef: {name: pg-creds, key: host} + host: + secretKeyRef: {name: pg-creds, key: host} resources: models: @@ -448,7 +362,7 @@ spec: autoscaling: minReplicas: 1 maxReplicas: 5 - targetCPUUtilizationPercentage: 80 + targetCPUUtilization: 80 storage: size: "10Gi" mountPath: "/.llama" @@ -508,68 +422,47 @@ spec: 1. Fetch LLSD CR โ”‚ โ–ผ -2. Resolve Distribution - โ”œโ”€โ”€ distribution.name โ†’ lookup in distributions.json - โ”‚ โ””โ”€โ”€ Check image-overrides in operator ConfigMap - โ”œโ”€โ”€ distribution.image โ†’ use directly - โ””โ”€โ”€ Record resolved image in status.resolvedDistribution - โ”‚ - โ–ผ -3. Validate Configuration (webhook + controller) +2. Validate Configuration โ”œโ”€โ”€ Check mutual exclusivity (providers vs overrideConfig) โ”œโ”€โ”€ Validate secret references exist - โ”œโ”€โ”€ Validate ConfigMap references exist - โ””โ”€โ”€ Validate provider ID references in resources + โ””โ”€โ”€ Validate ConfigMap references exist โ”‚ โ–ผ -4. Determine Config Source +3. Determine Config Source โ”œโ”€โ”€ If overrideConfig: Use referenced ConfigMap directly โ””โ”€โ”€ If providers/resources: Generate config โ”‚ โ–ผ -5. Obtain Base Config - โ”œโ”€โ”€ Check OCI labels on resolved image (Phase 2) - โ”œโ”€โ”€ Fall back to embedded config for distribution name (Phase 1) - โ””โ”€โ”€ Require overrideConfig if neither is available - โ”‚ - โ–ผ -6. Generate Configuration (if not using overrideConfig) - โ”œโ”€โ”€ Merge user providers over base config providers +4. Generate Configuration (if not using overrideConfig) + โ”œโ”€โ”€ Extract base config.yaml from distribution image + โ”œโ”€โ”€ Expand providers to full config.yaml format โ”œโ”€โ”€ Expand resources to registered_resources format โ”œโ”€โ”€ Apply storage configuration โ”œโ”€โ”€ Apply disabled APIs - โ”œโ”€โ”€ Resolve secretKeyRef to environment variables - โ””โ”€โ”€ Validate generated config structure + โ””โ”€โ”€ Resolve secretKeyRef to environment variables โ”‚ โ–ผ -7. Merge External Providers (from spec 001) - โ”œโ”€โ”€ Add external providers to generated config - โ””โ”€โ”€ Override on ID conflict (with warning) +5. Create/Update ConfigMap + โ”œโ”€โ”€ Generate ConfigMap with content hash in name + โ”œโ”€โ”€ Set owner reference + โ””โ”€โ”€ Create new ConfigMap (immutable pattern) โ”‚ โ–ผ -8. Compare with Current State - โ”œโ”€โ”€ Hash merged config against current ConfigMap - โ”œโ”€โ”€ If identical โ†’ skip update, no Pod restart - โ””โ”€โ”€ If different โ†’ proceed to update +6. Update Deployment + โ”œโ”€โ”€ Mount generated ConfigMap + โ”œโ”€โ”€ Inject environment variables for secrets + โ””โ”€โ”€ Add hash annotation for rollout trigger โ”‚ โ–ผ -9. Create ConfigMap + Update Deployment (atomic) - โ”œโ”€โ”€ Create new ConfigMap with content hash in name - โ”œโ”€โ”€ Set owner reference on ConfigMap - โ”œโ”€โ”€ Update Deployment atomically: - โ”‚ โ”œโ”€โ”€ Container image (from resolved distribution) - โ”‚ โ”œโ”€โ”€ ConfigMap volume mount - โ”‚ โ”œโ”€โ”€ Environment variables for secrets - โ”‚ โ””โ”€โ”€ Hash annotation for rollout trigger - โ””โ”€โ”€ On failure โ†’ preserve current Deployment, report error +7. Merge External Providers (from spec 001) + โ”œโ”€โ”€ Add external providers to config + โ””โ”€โ”€ Override on ID conflict (with warning) โ”‚ โ–ผ -10. Update Status - โ”œโ”€โ”€ Set phase - โ”œโ”€โ”€ Update conditions (ConfigGenerated, DeploymentUpdated, - โ”‚ Available) - โ”œโ”€โ”€ Record resolvedDistribution (image, configHash) - โ””โ”€โ”€ Record config generation details +8. Update Status + โ”œโ”€โ”€ Set phase + โ”œโ”€โ”€ Update conditions + โ””โ”€โ”€ Record config generation details ``` ## Configuration Tiers @@ -582,30 +475,6 @@ spec: ## Status Reporting -### Printer Columns - -The v1alpha2 CRD MUST define the following printer columns for `kubectl get` output (constitution ยง2.5): - -```go -//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" -//+kubebuilder:printcolumn:name="Distribution",type="string",JSONPath=".status.resolvedDistribution.image",priority=1 -//+kubebuilder:printcolumn:name="Config",type="string",JSONPath=".status.configGeneration.configMapName",priority=1 -//+kubebuilder:printcolumn:name="Providers",type="integer",JSONPath=".status.configGeneration.providerCount" -//+kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas" -//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -``` - -Default `kubectl get llsd` output: - -``` -NAME PHASE PROVIDERS AVAILABLE AGE -my-stack Ready 3 1 5m -``` - -Wide output (`kubectl get llsd -o wide`) includes `priority=1` columns (Distribution, Config). - -### Status Fields - The status MUST include: ```yaml @@ -616,22 +485,10 @@ status: status: "True" reason: ConfigGenerationSucceeded message: "Generated config.yaml with 3 providers and 2 models" - - type: DeploymentUpdated - status: "True" - reason: DeploymentUpdateSucceeded - message: "Deployment updated with config my-stack-config-abc123" - - type: Available - status: "True" - reason: MinimumReplicasAvailable - message: "1/1 replicas available with current config" - type: SecretsResolved status: "True" reason: AllSecretsFound message: "Resolved 2 secret references" - resolvedDistribution: - image: "docker.io/llamastack/distribution-starter@sha256:abc123" - configSource: embedded # or "oci-label" (Phase 2) - configHash: "sha256:def456" configGeneration: configMapName: my-stack-config-abc123 generatedAt: "2026-02-02T12:00:00Z" @@ -642,17 +499,9 @@ status: ## Security Considerations - **Secret Handling**: Secret values MUST only be passed via environment variables, never embedded in ConfigMap -- **Environment Variable Naming**: Use deterministic, prefixed names: `LLSD__` (e.g., `LLSD_VLLM_PRIMARY_API_KEY`). Provider ID is uppercased with hyphens replaced by underscores. +- **Environment Variable Naming**: Use deterministic, prefixed names: `LLSD__` (e.g., `LLSD_INFERENCE_API_KEY`) - **ConfigMap Permissions**: Generated ConfigMaps inherit namespace RBAC - **Image Extraction**: Config extraction from images uses read-only operations -- **Webhook Permissions**: The `ValidatingWebhookConfiguration` is a cluster-scoped resource, installed by OLM or kustomize during operator setup (not by the operator at runtime). This is a standard pattern for Kubernetes operators with admission webhooks and is an accepted deviation from constitution ยง1.1. The operator itself remains namespace-scoped at runtime. - -## Open Questions - -- ~~**OQ-001**~~: Resolved. `expose: {}` is treated as `expose: true` (see Edge Cases). -- ~~**OQ-002**~~: Resolved. Disabled API + provider config conflict produces a **warning** (not an error). The `disabled` list takes precedence: the provider config is accepted but ignored at runtime. Warning is logged and reported in status conditions. (From PR #242 review; resolved per edge case "Disabled APIs conflict with providers") -- ~~**OQ-003**~~: Resolved. Environment variable naming uses the **provider ID** (not provider type or API type): `LLSD__`. The provider ID is unique across all providers (enforced by FR-072), ensuring no collisions. For single providers without explicit `id`, the auto-generated ID from FR-035 is used. Examples: `LLSD_VLLM_PRIMARY_API_KEY`, `LLSD_PGVECTOR_HOST`. Characters not valid in env var names (hyphens) are replaced with underscores and uppercased. (From PR #242 review) -- **OQ-004**: Should the operator create a default LlamaStackDistribution instance when installed? This is uncommon for Kubernetes operators but could improve the getting-started experience. If adopted, it should be opt-in via operator configuration (e.g., a Helm value or OLM parameter). (From team discussion, 2026-02-10) ## References diff --git a/specs/002-operator-generated-config/tasks.md b/specs/002-operator-generated-config/tasks.md index 873e06a13..cad544b56 100644 --- a/specs/002-operator-generated-config/tasks.md +++ b/specs/002-operator-generated-config/tasks.md @@ -5,13 +5,13 @@ ## Task Overview -| Phase | Tasks | Priority | -|-------|-------|----------| -| Phase 1: CRD Schema | 8 tasks | P1 | -| Phase 2: Config Generation | 8 tasks | P1 | -| Phase 3: Controller Integration | 12 tasks | P1 | -| Phase 4: Conversion Webhook | 4 tasks | P2 | -| Phase 5: Testing & Docs | 6 tasks | P2 | +| Phase | Tasks | Priority | Estimated Effort | +|-------|-------|----------|------------------| +| Phase 1: CRD Schema | 8 tasks | P1 | Medium | +| Phase 2: Config Generation | 8 tasks | P1 | Large | +| Phase 3: Controller Integration | 8 tasks | P1 | Large | +| Phase 4: Conversion Webhook | 4 tasks | P2 | Medium | +| Phase 5: Testing & Docs | 5 tasks | P2 | Medium | --- @@ -31,7 +31,7 @@ Create the v1alpha2 API package with groupversion_info.go and base types. **Acceptance criteria**: - [ ] v1alpha2 package compiles -- [ ] GroupVersion is `llamastack.io/v1alpha2` +- [ ] GroupVersion is `llamastack.ai/v1alpha2` - [ ] Scheme registration works --- @@ -46,7 +46,7 @@ Define ProvidersSpec and ProviderConfig types with polymorphic support. **Types to define**: - `ProvidersSpec` (inference, safety, vectorIo, toolRuntime, telemetry) -- `ProviderConfig` (id, provider, endpoint, apiKey, settings) +- `ProviderConfig` (id, provider, endpoint, apiKey, host, settings) - `ProviderConfigOrList` (polymorphic wrapper using json.RawMessage) - `SecretKeyRef` (name, key) @@ -161,28 +161,19 @@ Create the main LlamaStackDistributionSpec with all sections and add CEL validat - `LlamaStackDistributionSpec` (distribution, providers, resources, storage, disabled, networking, workload, externalProviders, overrideConfig) - `LlamaStackDistribution` (main CRD type) - `LlamaStackDistributionList` -- `OverrideConfigSpec` (configMapName; must be in same namespace as CR) +- `OverrideConfigSpec` (configMapName, configMapNamespace) **CEL validations**: - Mutual exclusivity: providers vs overrideConfig - Mutual exclusivity: resources vs overrideConfig - Mutual exclusivity: storage vs overrideConfig -- Mutual exclusivity: disabled vs overrideConfig **Requirements covered**: FR-001, FR-002, FR-009, FR-013, FR-014, FR-070 -**Printer columns** (constitution ยง2.5): -- `Phase` (`.status.phase`) -- `Distribution` (`.status.resolvedDistribution.image`, priority=1, wide output only) -- `Config` (`.status.configGeneration.configMapName`, priority=1, wide output only) -- `Providers` (`.status.configGeneration.providerCount`) -- `Available` (`.status.availableReplicas`) -- `Age` (`.metadata.creationTimestamp`) - **Acceptance criteria**: - [ ] Complete spec structure compiles - [ ] CEL validations reject invalid combinations -- [ ] Printer columns defined: Phase, Providers, Available, Age (default); Distribution, Config (wide) +- [ ] Printer columns defined for kubectl output --- @@ -229,47 +220,63 @@ Create the pkg/config package directory structure with basic types. --- -### Task 2.2: Implement Base Config Resolution (Phased) +### Task 2.2: Implement Base Config Extraction (OCI Label Approach) **Priority**: P1 **Blocked by**: 2.1 **Description**: -Implement base config resolution with a phased approach. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override. +Implement extraction of base config.yaml from distribution images using OCI labels. +Uses `k8schain` for registry authentication (same credentials as kubelet). -**Files**: -- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic -- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) -- `Makefile` - Build-time validation target (`validate-configs`) - -**Phase 1 (MVP) Approach**: -1. Create `configs//config.yaml` for each distribution in `distributions.json` -2. Embed via `//go:embed configs` in the resolver package -3. On resolution: lookup embedded config by `distribution.name` -4. For `distribution.image` without OCI labels: require `overrideConfig` - -**Phase 2 (Enhancement) Approach**: -1. Add `pkg/config/oci_extractor.go` using `k8schain` for registry auth -2. Check OCI labels on resolved image first (takes precedence over embedded) -3. Fall back to embedded config if no labels found -4. Cache by image digest +**File**: `pkg/config/extractor.go` + +**Approach**: +1. Fetch image config blob (contains labels) using `crane.Config()` +2. Check for `io.llamastack.config.base64` label (inline config) +3. If not present, check for `io.llamastack.config.layer` + `.path` (layer reference) +4. Extract config from appropriate source +5. Cache by image digest + +**Dependencies**: +- `github.com/google/go-containerregistry/pkg/crane` +- `github.com/google/go-containerregistry/pkg/authn/k8schain` + +**Types**: +```go +type ConfigLocation struct { + Base64 string // Inline base64 encoded config + LayerDigest string // Layer digest containing config + Path string // Path within layer + Version string // Config schema version +} + +type ImageConfigExtractor struct { + k8sClient client.Client + namespace string + serviceAccount string + cache *sync.Map // digest -> BaseConfig +} +``` **Functions**: -- `NewBaseConfigResolver(distributionImages, imageOverrides) *BaseConfigResolver` -- `(r *BaseConfigResolver) Resolve(ctx, distribution) (*BaseConfig, string, error)` -- `(r *BaseConfigResolver) loadEmbeddedConfig(name) (*BaseConfig, error)` -- `(r *BaseConfigResolver) resolveImage(distribution) (string, error)` +- `NewImageConfigExtractor(client, namespace, sa) *ImageConfigExtractor` +- `(e *ImageConfigExtractor) Extract(ctx, imageRef) (*BaseConfig, error)` +- `(e *ImageConfigExtractor) getConfigLocation(ctx, imageRef, keychain) (*ConfigLocation, error)` +- `(e *ImageConfigExtractor) extractFromBase64(b64) (*BaseConfig, error)` +- `(e *ImageConfigExtractor) extractFromLayer(ctx, imageRef, layerDigest, path, keychain) (*BaseConfig, error)` -**Requirements covered**: FR-020, FR-027a through FR-027e (Phase 1), FR-027f through FR-027j (Phase 2), NFR-006 +**Requirements covered**: FR-020, FR-027a through FR-027f, NFR-006 + +**Alternative**: See `alternatives/init-container-extraction.md` for init container approach **Acceptance criteria**: -- [ ] Embedded configs loaded via `go:embed` for all named distributions -- [ ] `distribution.name` resolves to embedded config -- [ ] `distribution.image` without OCI labels returns clear error requiring `overrideConfig` -- [ ] Build-time validation ensures all distributions have configs -- [ ] (Phase 2) OCI label extraction takes precedence over embedded when available -- [ ] (Phase 2) Caching by image digest prevents repeated extraction -- [ ] Unit tests for resolution priority logic +- [ ] Can extract config from `io.llamastack.config.base64` label +- [ ] Can extract config from layer using `io.llamastack.config.layer` + `.path` labels +- [ ] Uses k8schain for registry authentication (respects imagePullSecrets) +- [ ] Caching by image digest prevents repeated extraction +- [ ] Clear error message when distribution image lacks config labels +- [ ] Unit tests for both extraction strategies --- @@ -345,8 +352,6 @@ Implement resource spec expansion to registered_resources format. - [ ] Model objects expand correctly - [ ] Default provider assignment works - [ ] Tools and shields expand correctly -- [ ] Tools fail with actionable error when no toolRuntime provider exists (user or base config) -- [ ] Shields fail with actionable error when no safety provider exists (user or base config) --- @@ -636,10 +641,8 @@ Add config generation status fields and conditions. **File**: `controllers/status.go` **New conditions**: -- `ConfigGenerated`: True when config successfully generated -- `DeploymentUpdated`: True when Deployment spec updated with current config -- `Available`: True when at least one Pod is ready with current config -- `SecretsResolved`: True when all secret references valid +- `ConfigGenerated` +- `SecretsResolved` **New status fields**: ```go @@ -650,190 +653,15 @@ type ConfigGenerationStatus struct { ResourceCount int ConfigVersion int } - -type ResolvedDistributionStatus struct { - Image string // Resolved image from distribution.name - ConfigSource string // "embedded" or "oci-label" - ConfigHash string // Hash of current base config -} ``` -**Requirements covered**: FR-020a, FR-099 - **Acceptance criteria**: - [ ] New conditions set correctly - [ ] Config generation details in status -- [ ] `resolvedDistribution` recorded in status - [ ] Status updated on each reconcile --- -### Task 3.9: Implement Validating Admission Webhook - -**Priority**: P1 -**Blocked by**: 1.7 - -**Description**: -Implement a validating webhook for constraints that cannot be expressed in CEL, complementing the CEL rules added in Phase 1 (Task 1.7). - -**File**: `api/v1alpha2/llamastackdistribution_webhook.go` - -**Validation logic**: -- Verify referenced Secrets exist in the namespace (fast admission-time feedback) -- Verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` -- Validate provider ID references in `resources.models[].provider` -- Cross-field semantic validation (e.g., model provider references valid provider IDs) - -**Configuration**: -- Webhook deployment via kustomize manifests (`config/webhook/`) -- Certificate management using cert-manager or operator-managed self-signed certs -- Failure policy: `Fail` (reject CR if webhook unreachable) - -```go -func (r *LlamaStackDistribution) ValidateCreate() (admission.Warnings, error) { - return r.validate() -} - -func (r *LlamaStackDistribution) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - return r.validate() -} - -func (r *LlamaStackDistribution) validate() (admission.Warnings, error) { - var allErrs field.ErrorList - // Validate secret references exist - // Validate ConfigMap references exist - // Validate provider ID cross-references - return nil, allErrs.ToAggregate() -} -``` - -**Requirements covered**: FR-076, FR-077, FR-078 - -**Acceptance criteria**: -- [ ] Webhook validates Secret existence at admission time -- [ ] Webhook validates ConfigMap references -- [ ] Webhook validates cross-field provider ID references -- [ ] Clear error messages with field paths -- [ ] Webhook kustomize manifests configured - ---- - -### Task 3.10: Implement Runtime Configuration Update Logic - -**Priority**: P1 -**Blocked by**: 3.3 - -**Description**: -On every reconciliation, compare the generated config hash with the currently deployed config hash. Only update the Deployment when content actually changes. On failure, preserve the current running Deployment. - -**File**: `controllers/llamastackdistribution_controller.go` - -**Logic**: -``` -Reconcile() -โ”œโ”€โ”€ Generate config (or use overrideConfig) -โ”œโ”€โ”€ Compute content hash of generated config -โ”œโ”€โ”€ Compare with status.configGeneration.configMapName hash -โ”œโ”€โ”€ If identical โ†’ skip update, no Pod restart -โ””โ”€โ”€ If different: - โ”œโ”€โ”€ Create new ConfigMap - โ”œโ”€โ”€ Update Deployment atomically (image + config + env) - โ”œโ”€โ”€ On success โ†’ update status - โ””โ”€โ”€ On failure โ†’ preserve current Deployment, report error -``` - -**Failure preservation**: -```go -func (r *Reconciler) reconcileConfig(ctx context.Context, instance *v1alpha2.LlamaStackDistribution) error { - generated, err := config.GenerateConfig(ctx, instance.Spec, resolvedImage) - if err != nil { - // Preserve current running state, report error - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: "ConfigGenerated", - Status: metav1.ConditionFalse, - Reason: "ConfigGenerationFailed", - Message: err.Error(), - }) - return nil // Don't requeue, let user fix the CR - } - // ... proceed with update -} -``` - -**Requirements covered**: FR-095, FR-096, FR-097 - -**Acceptance criteria**: -- [ ] Config hash comparison prevents unnecessary restarts -- [ ] Failed config generation preserves current Deployment -- [ ] Error reported in status conditions on failure -- [ ] Successful update reflected in status - ---- - -### Task 3.11: Implement Atomic Deployment Updates - -**Priority**: P1 -**Blocked by**: 3.10 - -**Description**: -When the Deployment needs updating, apply all changes (image, ConfigMap mount, env vars, hash annotation) in a single `client.Update()` call to prevent intermediate states where the running image and config are mismatched. - -**File**: `controllers/llamastackdistribution_controller.go` - -```go -func (r *Reconciler) updateDeploymentAtomically( - ctx context.Context, - deployment *appsv1.Deployment, - resolvedImage string, - configMapName string, - envVars []corev1.EnvVar, - configHash string, -) error { - // Update all fields in one mutation - deployment.Spec.Template.Spec.Containers[0].Image = resolvedImage - // ... update ConfigMap volume, env vars, hash annotation - return r.Client.Update(ctx, deployment) -} -``` - -**Operator upgrade handling**: When the operator is upgraded and `distributions.json` maps a name to a new image, the reconciler detects the image change via `status.resolvedDistribution.image` comparison and triggers an atomic update with the new base config. - -**Requirements covered**: FR-098, FR-100, FR-101 - -**Acceptance criteria**: -- [ ] Image + ConfigMap + env vars updated in single API call -- [ ] No intermediate state where image and config mismatch -- [ ] Operator upgrade triggers atomic update when image changes -- [ ] Failed update preserves current Deployment (see FR-097) - ---- - -### Task 3.12: Implement Distribution Resolution Tracking - -**Priority**: P1 -**Blocked by**: 3.3 - -**Description**: -Track the resolved image in `status.resolvedDistribution` so the controller can detect changes across reconciliations (e.g., after operator upgrade where `distributions.json` maps a name to a new image). - -**File**: `controllers/llamastackdistribution_controller.go` - -**Logic**: -1. Resolve `distribution.name` to concrete image using `distributions.json` + `image-overrides` -2. Compare with `status.resolvedDistribution.image` -3. If different: regenerate config with new base, update atomically -4. Record new resolved image in status - -**Requirements covered**: FR-020a, FR-020b, FR-020c - -**Acceptance criteria**: -- [ ] Resolved image recorded in `status.resolvedDistribution` -- [ ] Image change detected between reconciliations -- [ ] Image change triggers config regeneration and atomic update -- [ ] Config source ("embedded" or "oci-label") recorded in status - ---- - ## Phase 4: Conversion Webhook ### Task 4.1: Implement v1alpha2 Hub @@ -847,19 +675,21 @@ Mark v1alpha2 as the conversion hub (storage version). **File**: `api/v1alpha2/llamastackdistribution_conversion.go` **Implementation**: - -In controller-runtime, the Hub only implements a marker method. Conversion logic lives on the Spoke (v1alpha1). - ```go -// Hub marks v1alpha2 as the storage version for conversion. -func (dst *LlamaStackDistribution) Hub() {} +func (src *LlamaStackDistribution) ConvertTo(dstRaw conversion.Hub) error { + return nil // v1alpha2 is hub +} + +func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { + return nil // v1alpha2 is hub +} ``` **Requirements covered**: FR-081 **Acceptance criteria**: -- [ ] v1alpha2 implements `conversion.Hub` interface via `Hub()` marker method -- [ ] No conversion logic on the hub (all conversion is on the v1alpha1 spoke) +- [ ] v1alpha2 implements Hub interface +- [ ] No-op conversion for hub --- @@ -981,18 +811,11 @@ Write integration tests for v1alpha2 controller logic. - Network exposure - Override config - Validation errors -- Runtime config updates (US8): CR update triggers config regeneration -- Atomic Deployment updates: image + config updated together -- Webhook validation: invalid references rejected at admission -- Distribution resolution tracking: operator upgrade triggers update -- Config generation failure: current Deployment preserved **Acceptance criteria**: -- [ ] All user stories have tests (including US8) +- [ ] All user stories have tests - [ ] Edge cases covered - [ ] Error scenarios tested -- [ ] Webhook validation tested -- [ ] Atomic update scenarios tested --- @@ -1061,30 +884,6 @@ Update documentation for v1alpha2. --- -### Task 5.6: Performance Benchmarks - -**Priority**: P2 -**Blocked by**: 2.8 - -**Description**: -Write Go benchmark tests to verify config generation completes within the NFR-002 threshold (5 seconds for typical configurations). - -**File**: `pkg/config/config_benchmark_test.go` - -**Benchmark scenarios**: -- Single provider, single model (minimal config) -- 5 providers, 10 models, storage, networking (typical production) -- 10 providers, 50 models, all features enabled (stress test) - -**Requirements covered**: NFR-002 - -**Acceptance criteria**: -- [ ] Benchmark tests pass under 5 seconds for typical configuration -- [ ] Results documented in test output -- [ ] CI runs benchmarks (optional, for regression detection) - ---- - ## Task Dependencies Graph ``` @@ -1106,18 +905,12 @@ Phase 2 (Config Generation) Phase 3 (Controller) โ”œโ”€โ”€ 3.1 โ”€โ–บ 3.2, 3.5, 3.6 โ”œโ”€โ”€ 3.2 โ”€โ–บ 3.3 -โ”œโ”€โ”€ 3.3 โ”€โ–บ 3.4, 3.7, 3.8, 3.10, 3.12 -โ”œโ”€โ”€ 3.10 โ”€โ–บ 3.11 -โ”œโ”€โ”€ 3.9 (blocked by 1.7) -โ””โ”€โ”€ 3.12 (parallel with 3.10) +โ”œโ”€โ”€ 3.3 โ”€โ–บ 3.4, 3.7, 3.8 +โ””โ”€โ”€ ... Phase 4 (Webhook) โ””โ”€โ”€ 4.1 โ”€โ–บ 4.2, 4.3 โ”€โ–บ 4.4 Phase 5 (Testing) -โ”œโ”€โ”€ 5.1 (blocked by Phase 2) -โ”œโ”€โ”€ 5.2 (blocked by Phase 3) -โ”œโ”€โ”€ 5.3 (blocked by Phase 4) -โ”œโ”€โ”€ 5.4, 5.5 (blocked by Phase 3, 4) -โ””โ”€โ”€ 5.6 (blocked by 2.8) +โ””โ”€โ”€ Depends on respective phases ``` From ff3b2c51e699dd93982ea9ef12135e9cc2801a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Wed, 4 Feb 2026 09:38:25 +0100 Subject: [PATCH 2/9] docs(spec): add executive brief for spec review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a condensed 2-page summary of the v1alpha2 spec for easier team review. Includes before/after example, key design decisions, and specific questions for reviewers. Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- specs/002-operator-generated-config/BRIEF.md | 119 +++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 specs/002-operator-generated-config/BRIEF.md diff --git a/specs/002-operator-generated-config/BRIEF.md b/specs/002-operator-generated-config/BRIEF.md new file mode 100644 index 000000000..9eb706471 --- /dev/null +++ b/specs/002-operator-generated-config/BRIEF.md @@ -0,0 +1,119 @@ +# Spec Brief: Operator-Generated Config (v1alpha2) + +**Full spec:** [spec.md](spec.md) | **Status:** Draft | **Priority:** P1 + +## Problem Statement + +Users currently must provide a complete `config.yaml` via ConfigMap to configure LlamaStack. This requires deep knowledge of the config schema and results in verbose, error-prone YAML. + +## Solution + +Introduce v1alpha2 API with high-level abstractions that the operator expands into a complete `config.yaml`. Users write 10-20 lines instead of 200+. + +## Before/After Example + +**Before (v1alpha1):** User provides 200+ line ConfigMap manually + +**After (v1alpha2):** +```yaml +apiVersion: llamastack.ai/v1alpha2 +kind: LlamaStackDistribution +metadata: + name: my-stack +spec: + distribution: + name: starter + providers: + inference: + provider: vllm + endpoint: "http://vllm:8000" + apiKey: + secretKeyRef: {name: vllm-creds, key: token} + resources: + models: ["llama3.2-8b"] + storage: + sql: + type: postgres + connectionString: + secretKeyRef: {name: pg-creds, key: url} +``` + +## Key Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Config extraction | OCI image labels | Single-phase reconcile, works with imagePullSecrets | +| Secret handling | Environment variables | Never embed secrets in ConfigMap | +| Multiple providers | Explicit `id` required | Avoid ambiguity in provider references | +| Backward compat | Conversion webhook | v1alpha1 CRs continue working | +| Override escape hatch | `overrideConfig` field | Power users can bypass generation | + +## Configuration Tiers + +| Tier | Users | Mechanism | +|------|-------|-----------| +| Simple (80%) | Most users | Inline provider fields | +| Advanced (15%) | Platform engineers | Per-provider `settings` | +| Full Control (5%) | Power users | ConfigMap override | + +## New Spec Sections + +``` +spec: + distribution: # Image source (name or direct image) + providers: # Inference, safety, vectorIo, toolRuntime, telemetry + resources: # Models, tools, shields to register + storage: # KV (sqlite/redis) and SQL (sqlite/postgres) + disabled: # APIs to disable + networking: # Port, TLS, expose, allowedFrom + workload: # Replicas, resources, autoscaling, PDB + overrideConfig: # Escape hatch: use ConfigMap directly +``` + +## What Reviewers Should Focus On + +1. **API Design**: Does the field structure make sense? Any awkward names? +2. **Polymorphic Fields**: Single object vs list forms (providers, models) +3. **Storage Abstraction**: Is kv/sql split intuitive? +4. **Edge Cases**: Are the 6 documented edge cases reasonable? +5. **External Dependencies**: Is the OCI label approach for base config extraction acceptable? + +## Requirements Summary + +| Category | Count | Coverage | +|----------|-------|----------| +| CRD Schema | FR-001 to FR-014 | All new fields defined | +| Config Generation | FR-020 to FR-029 | Extraction, merging, versioning | +| Providers | FR-030 to FR-035 | Field mapping, ID generation | +| Resources | FR-040 to FR-044 | Models, tools, shields | +| Storage | FR-050 to FR-053 | KV and SQL backends | +| Networking | FR-060 to FR-066 | Port, TLS, expose, NetworkPolicy | +| Validation | FR-070 to FR-075 | CEL rules, secret/ConfigMap checks | +| Conversion | FR-080 to FR-083 | v1alpha1 โ†” v1alpha2 webhook | +| Integration | FR-090 to FR-092 | Spec 001 external providers | + +## User Stories (P1 only) + +1. **Simple Inference**: Deploy with just `providers.inference` config +2. **Multiple Providers**: Configure primary + fallback providers +3. **Resource Registration**: Register models/tools declaratively +4. **State Storage**: Configure PostgreSQL for persistence + +## Dependencies + +- **Spec 001**: External providers merge into generated config +- **Distribution images**: Must include OCI labels with base config + +## Open Questions for Review + +1. Should `expose: {}` (empty object) be treated as `expose: true`? +2. Should disabled API + provider config conflict cause validation error or warning? +3. Is the `LLSD__` env var naming convention clear? + +## Implementation Estimate + +5 phases, 33 tasks (see [tasks.md](tasks.md) for details) + +--- + +**Ready for detailed review?** See [spec.md](spec.md) for full requirements. From ef86276a1a5c866ceeabf7eb72627da28a677aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Wed, 4 Feb 2026 11:47:39 +0100 Subject: [PATCH 3/9] docs(spec): rename BRIEF.md to review_summary.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- specs/002-operator-generated-config/BRIEF.md | 119 ------------------- 1 file changed, 119 deletions(-) delete mode 100644 specs/002-operator-generated-config/BRIEF.md diff --git a/specs/002-operator-generated-config/BRIEF.md b/specs/002-operator-generated-config/BRIEF.md deleted file mode 100644 index 9eb706471..000000000 --- a/specs/002-operator-generated-config/BRIEF.md +++ /dev/null @@ -1,119 +0,0 @@ -# Spec Brief: Operator-Generated Config (v1alpha2) - -**Full spec:** [spec.md](spec.md) | **Status:** Draft | **Priority:** P1 - -## Problem Statement - -Users currently must provide a complete `config.yaml` via ConfigMap to configure LlamaStack. This requires deep knowledge of the config schema and results in verbose, error-prone YAML. - -## Solution - -Introduce v1alpha2 API with high-level abstractions that the operator expands into a complete `config.yaml`. Users write 10-20 lines instead of 200+. - -## Before/After Example - -**Before (v1alpha1):** User provides 200+ line ConfigMap manually - -**After (v1alpha2):** -```yaml -apiVersion: llamastack.ai/v1alpha2 -kind: LlamaStackDistribution -metadata: - name: my-stack -spec: - distribution: - name: starter - providers: - inference: - provider: vllm - endpoint: "http://vllm:8000" - apiKey: - secretKeyRef: {name: vllm-creds, key: token} - resources: - models: ["llama3.2-8b"] - storage: - sql: - type: postgres - connectionString: - secretKeyRef: {name: pg-creds, key: url} -``` - -## Key Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Config extraction | OCI image labels | Single-phase reconcile, works with imagePullSecrets | -| Secret handling | Environment variables | Never embed secrets in ConfigMap | -| Multiple providers | Explicit `id` required | Avoid ambiguity in provider references | -| Backward compat | Conversion webhook | v1alpha1 CRs continue working | -| Override escape hatch | `overrideConfig` field | Power users can bypass generation | - -## Configuration Tiers - -| Tier | Users | Mechanism | -|------|-------|-----------| -| Simple (80%) | Most users | Inline provider fields | -| Advanced (15%) | Platform engineers | Per-provider `settings` | -| Full Control (5%) | Power users | ConfigMap override | - -## New Spec Sections - -``` -spec: - distribution: # Image source (name or direct image) - providers: # Inference, safety, vectorIo, toolRuntime, telemetry - resources: # Models, tools, shields to register - storage: # KV (sqlite/redis) and SQL (sqlite/postgres) - disabled: # APIs to disable - networking: # Port, TLS, expose, allowedFrom - workload: # Replicas, resources, autoscaling, PDB - overrideConfig: # Escape hatch: use ConfigMap directly -``` - -## What Reviewers Should Focus On - -1. **API Design**: Does the field structure make sense? Any awkward names? -2. **Polymorphic Fields**: Single object vs list forms (providers, models) -3. **Storage Abstraction**: Is kv/sql split intuitive? -4. **Edge Cases**: Are the 6 documented edge cases reasonable? -5. **External Dependencies**: Is the OCI label approach for base config extraction acceptable? - -## Requirements Summary - -| Category | Count | Coverage | -|----------|-------|----------| -| CRD Schema | FR-001 to FR-014 | All new fields defined | -| Config Generation | FR-020 to FR-029 | Extraction, merging, versioning | -| Providers | FR-030 to FR-035 | Field mapping, ID generation | -| Resources | FR-040 to FR-044 | Models, tools, shields | -| Storage | FR-050 to FR-053 | KV and SQL backends | -| Networking | FR-060 to FR-066 | Port, TLS, expose, NetworkPolicy | -| Validation | FR-070 to FR-075 | CEL rules, secret/ConfigMap checks | -| Conversion | FR-080 to FR-083 | v1alpha1 โ†” v1alpha2 webhook | -| Integration | FR-090 to FR-092 | Spec 001 external providers | - -## User Stories (P1 only) - -1. **Simple Inference**: Deploy with just `providers.inference` config -2. **Multiple Providers**: Configure primary + fallback providers -3. **Resource Registration**: Register models/tools declaratively -4. **State Storage**: Configure PostgreSQL for persistence - -## Dependencies - -- **Spec 001**: External providers merge into generated config -- **Distribution images**: Must include OCI labels with base config - -## Open Questions for Review - -1. Should `expose: {}` (empty object) be treated as `expose: true`? -2. Should disabled API + provider config conflict cause validation error or warning? -3. Is the `LLSD__` env var naming convention clear? - -## Implementation Estimate - -5 phases, 33 tasks (see [tasks.md](tasks.md) for details) - ---- - -**Ready for detailed review?** See [spec.md](spec.md) for full requirements. From 72162625a32b98b6bd6550b69ea7149abda5bfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Feb 2026 23:09:56 +0100 Subject: [PATCH 4/9] docs(spec): refine v1alpha2 spec after cross-artifact analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply fixes from consistency analysis across spec, plan, and tasks: - Fix expose:{} handling in plan (ShouldExposeRoute returned false) - Align autoscaling field name to targetCPUUtilizationPercentage - Fix hub/spoke conversion pattern (Hub() marker, not ConvertTo) - Restructure plan section 2.2 for phased base config approach - Add v1alpha2 printer columns (Phase, Providers, Available, Age) - Add contracts, data-model, quickstart, and research artifacts - Update review_summary with changelog for reviewers Assisted-By: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../alternatives/init-container-extraction.md | 2 +- specs/002-operator-generated-config/plan.md | 299 ++++++++++++--- specs/002-operator-generated-config/spec.md | 255 ++++++++++--- specs/002-operator-generated-config/tasks.md | 351 ++++++++++++++---- 4 files changed, 731 insertions(+), 176 deletions(-) diff --git a/specs/002-operator-generated-config/alternatives/init-container-extraction.md b/specs/002-operator-generated-config/alternatives/init-container-extraction.md index 770d974e4..d23a52f42 100644 --- a/specs/002-operator-generated-config/alternatives/init-container-extraction.md +++ b/specs/002-operator-generated-config/alternatives/init-container-extraction.md @@ -96,7 +96,7 @@ func (c *BaseConfigCache) GetOrCreate(ctx context.Context, name, image string) ( } // Verify image matches (invalidate cache if distribution changed) - if cm.Annotations["llamastack.ai/source-image"] != image { + if cm.Annotations["llamastack.io/source-image"] != image { // Image changed, delete cached config to trigger re-extraction if err := c.client.Delete(ctx, &cm); err != nil { return nil, err diff --git a/specs/002-operator-generated-config/plan.md b/specs/002-operator-generated-config/plan.md index 7ea5eff8f..e80fae684 100644 --- a/specs/002-operator-generated-config/plan.md +++ b/specs/002-operator-generated-config/plan.md @@ -1,12 +1,105 @@ # Implementation Plan: Operator-Generated Server Configuration (v1alpha2) -**Spec**: 002-operator-generated-config -**Created**: 2026-02-02 +**Branch**: `002-operator-generated-config` | **Date**: 2026-02-02 | **Spec**: [spec.md](spec.md) **Status**: Ready for Implementation -## Overview +## Summary + +Introduce a v1alpha2 API version for the LlamaStackDistribution CRD that enables the operator to generate server configuration (config.yaml) from a high-level, abstracted CR specification. Users configure providers, resources, and storage with minimal YAML while the operator handles config generation, secret resolution, and atomic Deployment updates. + +## Technical Context + +**Language/Version**: Go 1.25 (go.mod) +**Primary Dependencies**: controller-runtime v0.22.4, kubebuilder, kustomize/api v0.21.0, client-go v0.34.3, go-containerregistry v0.20.7 +**Storage**: Kubernetes ConfigMaps (generated), Secrets (referenced via secretKeyRef) +**Testing**: Go test, envtest (controller-runtime), testify v1.11.1 +**Target Platform**: Kubernetes 1.30+ +**Project Type**: Kubernetes operator (single binary) +**Performance Goals**: Config generation < 5 seconds (NFR-002) +**Constraints**: Namespace-scoped RBAC (constitution ยง1.1), air-gapped registry support, deterministic output (NFR-001) +**Scale/Scope**: Single CRD with 2 API versions, ~8 new Go packages + +## Constitution Check + +*GATE: PASS (1 documented deviation, 0 unresolved violations)* + +| # | Principle | Status | Notes | +|---|-----------|--------|-------| +| ยง1.1 Namespace-Scoped | DEVIATION | ValidatingWebhookConfiguration is cluster-scoped (standard operator pattern). Documented in spec.md Security Considerations. | +| ยง1.2 Idempotent Reconciliation | PASS | Deterministic config generation (NFR-001). Hash-based change detection. | +| ยง1.3 Owner References | PASS | FR-025 requires owner refs on generated ConfigMaps. | +| ยง2.1 Kubebuilder Validation | PASS | CEL (FR-070-072), webhook (FR-076-078), kubebuilder tags. | +| ยง2.2 Optional Fields | PASS | Pointer types for optional structs throughout. | +| ยง2.3 Defaults | PASS | Constants for DefaultServerPort, storage type defaults. | +| ยง2.4 Status Subresource | PASS | New conditions: ConfigGenerated, DeploymentUpdated, Available, SecretsResolved. | +| ยง3.2 Conditions | PASS | Standard metav1.Condition with defined constants for types, reasons, messages. | +| ยง4.1 Error Wrapping | PASS | All errors wrapped with %w and context. | +| ยง6.1 Table-Driven Tests | PASS | Test plan follows constitution patterns. | +| ยง6.4 Builder Pattern | PASS | Existing test builders extended for v1alpha2. | +| ยง13.2 AI Attribution | PASS | Assisted-by format (no Co-Authored-By). | + +## Project Structure + +### Documentation (this feature) + +```text +specs/002-operator-generated-config/ +โ”œโ”€โ”€ spec.md # Feature specification +โ”œโ”€โ”€ plan.md # This file +โ”œโ”€โ”€ research.md # Phase 0 research decisions +โ”œโ”€โ”€ data-model.md # Entity definitions and relationships +โ”œโ”€โ”€ quickstart.md # Usage examples +โ”œโ”€โ”€ contracts/ # Interface contracts +โ”‚ โ”œโ”€โ”€ crd-schema.yaml +โ”‚ โ”œโ”€โ”€ config-generation.yaml +โ”‚ โ””โ”€โ”€ status-conditions.yaml +โ”œโ”€โ”€ tasks.md # Implementation tasks +โ”œโ”€โ”€ review_summary.md # Executive brief +โ””โ”€โ”€ alternatives/ # Alternative approaches evaluated +``` -This plan outlines the implementation strategy for the Operator-Generated Server Configuration feature, introducing the v1alpha2 API version with config generation capabilities. +### Source Code (repository root) + +```text +api/ +โ”œโ”€โ”€ v1alpha1/ # Existing types + conversion spoke +โ”‚ โ”œโ”€โ”€ llamastackdistribution_types.go +โ”‚ โ””โ”€โ”€ llamastackdistribution_conversion.go # New: v1alpha1 spoke +โ””โ”€โ”€ v1alpha2/ # New API version + โ”œโ”€โ”€ groupversion_info.go + โ”œโ”€โ”€ llamastackdistribution_types.go + โ”œโ”€โ”€ llamastackdistribution_webhook.go # Validating webhook + โ”œโ”€โ”€ llamastackdistribution_conversion.go # Hub (no-op) + โ””โ”€โ”€ zz_generated.deepcopy.go # Generated + +pkg/config/ # New: config generation engine +โ”œโ”€โ”€ config.go # Main orchestration +โ”œโ”€โ”€ generator.go # YAML generation +โ”œโ”€โ”€ resolver.go # Base config resolution +โ”œโ”€โ”€ provider.go # Provider expansion +โ”œโ”€โ”€ resource.go # Resource expansion +โ”œโ”€โ”€ storage.go # Storage configuration +โ”œโ”€โ”€ secret_resolver.go # Secret reference resolution +โ”œโ”€โ”€ version.go # Config schema version handling +โ”œโ”€โ”€ types.go # Internal config types +โ””โ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction + +configs/ # New: embedded default configs +โ”œโ”€โ”€ starter/config.yaml +โ”œโ”€โ”€ remote-vllm/config.yaml +โ”œโ”€โ”€ meta-reference-gpu/config.yaml +โ””โ”€โ”€ postgres-demo/config.yaml + +controllers/ # Extended for v1alpha2 +โ”œโ”€โ”€ llamastackdistribution_controller.go # Updated reconciliation +โ””โ”€โ”€ status.go # New conditions + +config/webhook/ # New: webhook kustomize config +โ””โ”€โ”€ manifests.yaml + +tests/e2e/ # Extended +โ””โ”€โ”€ config_generation_test.go # New: v1alpha2 e2e tests +``` ## Implementation Phases @@ -60,7 +153,6 @@ type ProviderConfig struct { Provider string `json:"provider"` Endpoint string `json:"endpoint,omitempty"` ApiKey *SecretKeyRef `json:"apiKey,omitempty"` - Host *SecretKeyRef `json:"host,omitempty"` Settings map[string]interface{} `json:"settings,omitempty"` } ``` @@ -156,6 +248,7 @@ type WorkloadOverrides struct { // +kubebuilder:validation:XValidation:rule="!(has(self.providers) && has(self.overrideConfig))",message="providers and overrideConfig are mutually exclusive" // +kubebuilder:validation:XValidation:rule="!(has(self.resources) && has(self.overrideConfig))",message="resources and overrideConfig are mutually exclusive" // +kubebuilder:validation:XValidation:rule="!(has(self.storage) && has(self.overrideConfig))",message="storage and overrideConfig are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.disabled) && has(self.overrideConfig))",message="disabled and overrideConfig are mutually exclusive" ``` #### 1.8 Generate CRD Manifests @@ -197,7 +290,8 @@ make manifests pkg/config/ โ”œโ”€โ”€ config.go # Main orchestration โ”œโ”€โ”€ generator.go # YAML generation -โ”œโ”€โ”€ extractor.go # Base config extraction from images +โ”œโ”€โ”€ resolver.go # Base config resolution (embedded + OCI) +โ”œโ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction โ”œโ”€โ”€ provider.go # Provider expansion โ”œโ”€โ”€ resource.go # Resource expansion โ”œโ”€โ”€ storage.go # Storage configuration @@ -206,17 +300,142 @@ pkg/config/ โ””โ”€โ”€ types.go # Internal config types ``` -#### 2.2 Implement Base Config Extraction (OCI Label Approach) +#### 2.2 Implement Base Config Resolution (Phased) -**File**: `pkg/config/extractor.go` +**Files**: +- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic +- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) +- `pkg/config/oci_extractor.go` - Phase 2: OCI label extraction -**Approach**: Extract the distribution's base `config.yaml` from OCI image labels, using the `k8schain` authenticator for registry access. This enables single-phase reconciliation and works with imagePullSecrets in air-gapped environments. +**Approach**: Base config resolution follows a phased strategy. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`, requiring no changes to distribution image builds. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override when distribution images support it. -> **Alternative**: An init container approach is documented in `alternatives/init-container-extraction.md` for cases where OCI labels are not available. +> **Alternative**: An init container approach is documented in `alternatives/init-container-extraction.md` for cases where neither embedded configs nor OCI labels are available. -**OCI Label Convention**: +**Resolution Priority**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. (Phase 2) Check OCI labels on resolved image โ”‚ +โ”‚ โ””โ”€โ”€ If present: extract config from labels โ”‚ +โ”‚ (takes precedence over embedded) โ”‚ +โ”‚ โ”‚ +โ”‚ 2. (Phase 1) Check embedded configs for distribution.name โ”‚ +โ”‚ โ””โ”€โ”€ If found: use go:embed config for that distribution โ”‚ +โ”‚ โ”‚ +โ”‚ 3. No config available: โ”‚ +โ”‚ โ”œโ”€โ”€ distribution.name โ†’ should not happen (build error) โ”‚ +โ”‚ โ””โ”€โ”€ distribution.image โ†’ require overrideConfig โ”‚ +โ”‚ (set ConfigGenerated=False, reason BaseConfigReq.) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +##### Phase 1: Embedded Default Configs (MVP) + +The operator binary embeds default configs for all named distributions via `go:embed`: + +```go +// pkg/config/resolver.go -Distribution images embed config.yaml in OCI labels using a tiered strategy: +import "embed" + +//go:embed configs +var embeddedConfigs embed.FS + +type BaseConfigResolver struct { + distributionImages map[string]string // from distributions.json + imageOverrides map[string]string // from operator ConfigMap + ociExtractor *ImageConfigExtractor // nil in Phase 1 +} + +func NewBaseConfigResolver(distImages, overrides map[string]string) *BaseConfigResolver { + return &BaseConfigResolver{ + distributionImages: distImages, + imageOverrides: overrides, + } +} + +func (r *BaseConfigResolver) Resolve(ctx context.Context, dist DistributionSpec) (*BaseConfig, string, error) { + // Resolve distribution to concrete image reference + image, err := r.resolveImage(dist) + if err != nil { + return nil, "", err + } + + // Phase 2: Check OCI labels first (when ociExtractor is configured) + if r.ociExtractor != nil { + config, err := r.ociExtractor.Extract(ctx, image) + if err == nil { + return config, image, nil + } + // Fall through to embedded if OCI extraction fails + log.FromContext(ctx).V(1).Info("OCI config extraction failed, falling back to embedded", + "image", image, "error", err) + } + + // Phase 1: Use embedded config for named distributions + if dist.Name != "" { + config, err := r.loadEmbeddedConfig(dist.Name) + if err != nil { + return nil, "", fmt.Errorf("failed to load embedded config for distribution %q: %w", dist.Name, err) + } + return config, image, nil + } + + // distribution.image without OCI labels or embedded config + return nil, "", fmt.Errorf("direct image references require either overrideConfig.configMapName or OCI config labels on the image") +} + +func (r *BaseConfigResolver) loadEmbeddedConfig(name string) (*BaseConfig, error) { + data, err := embeddedConfigs.ReadFile(fmt.Sprintf("configs/%s/config.yaml", name)) + if err != nil { + return nil, fmt.Errorf("no embedded config for distribution %q: %w", name, err) + } + + var config BaseConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("invalid embedded config for distribution %q: %w", name, err) + } + + return &config, nil +} + +func (r *BaseConfigResolver) resolveImage(dist DistributionSpec) (string, error) { + if dist.Image != "" { + return dist.Image, nil + } + + // Check image-overrides first (downstream builds) + if override, ok := r.imageOverrides[dist.Name]; ok { + return override, nil + } + + // Look up in distributions.json + if image, ok := r.distributionImages[dist.Name]; ok { + return image, nil + } + + return "", fmt.Errorf("unknown distribution name %q: not found in distributions.json", dist.Name) +} +``` + +**Embedded config directory** (created at build time): +``` +configs/ +โ”œโ”€โ”€ starter/config.yaml +โ”œโ”€โ”€ remote-vllm/config.yaml +โ”œโ”€โ”€ meta-reference-gpu/config.yaml +โ””โ”€โ”€ postgres-demo/config.yaml +``` + +**Air-gapped support**: Embedded configs work regardless of registry access. The `image-overrides` mechanism allows downstream builds (e.g., RHOAI) to remap distribution names to internal registry images without rebuilding the operator. + +##### Phase 2: OCI Label Extraction (Enhancement) + +**File**: `pkg/config/oci_extractor.go` + +When distribution images include OCI config labels, the extracted config takes precedence over embedded defaults. This enables `distribution.image` usage without `overrideConfig`. + +**OCI Label Convention**: | Label | Purpose | When Used | |-------|---------|-----------| @@ -225,22 +444,6 @@ Distribution images embed config.yaml in OCI labels using a tiered strategy: | `io.llamastack.config.path` | Path within the layer | Used with layer reference | | `io.llamastack.config.version` | Config schema version | Always | -**Extraction Priority**: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 1. Check io.llamastack.config.base64 โ”‚ -โ”‚ โ””โ”€โ”€ If present: decode and return (~10KB manifest fetch)โ”‚ -โ”‚ โ”‚ -โ”‚ 2. Check io.llamastack.config.layer + .path โ”‚ -โ”‚ โ””โ”€โ”€ If present: fetch specific layer, extract file โ”‚ -โ”‚ (~10-100KB single layer fetch) โ”‚ -โ”‚ โ”‚ -โ”‚ 3. No labels: Return error with guidance โ”‚ -โ”‚ โ””โ”€โ”€ Distribution image must include config labels โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - **Registry Authentication**: Uses `k8schain` from `go-containerregistry` to authenticate the same way kubelet does: @@ -261,7 +464,7 @@ import ( **Implementation**: ```go -// pkg/config/extractor.go +// pkg/config/oci_extractor.go type ConfigLocation struct { Base64 string // Inline base64 encoded config @@ -308,7 +511,7 @@ func (e *ImageConfigExtractor) Extract(ctx context.Context, imageRef string) (*B } // Fetch config location from image labels - loc, err := e.getConfigLocation(ctx, imageRef, keychain) + loc, err := e.getConfigLocation(imageRef, keychain) if err != nil { return nil, err } @@ -338,7 +541,7 @@ func (e *ImageConfigExtractor) Extract(ctx context.Context, imageRef string) (*B return config, nil } -func (e *ImageConfigExtractor) getConfigLocation(ctx context.Context, imageRef string, kc authn.Keychain) (*ConfigLocation, error) { +func (e *ImageConfigExtractor) getConfigLocation(imageRef string, kc authn.Keychain) (*ConfigLocation, error) { configJSON, err := crane.Config(imageRef, crane.WithAuthFromKeychain(kc)) if err != nil { return nil, fmt.Errorf("failed to fetch image config: %w", err) @@ -427,9 +630,9 @@ func (e *ImageConfigExtractor) extractFromLayer( } ``` -**Distribution Image Build Integration**: +**Distribution Image Build Integration** (Phase 2): -Labels are added post-build using `crane mutate` (solves the chicken-and-egg problem): +Labels are added post-build using `crane mutate` (solves the chicken-and-egg problem where layer digests are only known after build): ```bash #!/bin/bash @@ -479,7 +682,7 @@ fi - Labels added after build, so layer digest is known - Works with any registry that supports OCI manifests -**Air-Gapped / OpenShift Support**: +**Air-Gapped / OpenShift Support** (Phase 2): The `k8schain` authenticator handles: - imagePullSecrets from ServiceAccount @@ -509,9 +712,9 @@ The `k8schain` authenticator handles: - In-memory caching by digest (fast for repeated reconciles) **Cons**: -- Requires distribution images to include config labels +- Phase 2 requires distribution images to include config labels - Requires `go-containerregistry` dependency -- Distribution build process must use `crane mutate` +- Distribution build process must use `crane mutate` (Phase 2) #### 2.3 Implement Provider Expansion @@ -730,10 +933,9 @@ func (r *Reconciler) ShouldExposeRoute(spec *v1alpha2.NetworkingSpec) bool { if spec.Expose.Enabled != nil { return *spec.Expose.Enabled } - if spec.Expose.Hostname != "" { - return true - } - return false + // expose: {} (non-nil pointer, all zero-valued fields) is treated as + // expose: true per edge case "Polymorphic expose with empty object" + return true } ``` @@ -800,18 +1002,13 @@ type ConfigGenerationStatus struct { **File**: `api/v1alpha2/llamastackdistribution_conversion.go` -**Approach**: v1alpha2 is the hub (storage version) +**Approach**: v1alpha2 is the hub (storage version). In controller-runtime, the Hub type only implements a `Hub()` marker method. Conversion logic (`ConvertTo`/`ConvertFrom`) lives on the Spoke (v1alpha1). ```go -func (src *LlamaStackDistribution) ConvertTo(dstRaw conversion.Hub) error { - // v1alpha2 is hub, this is a no-op - return nil -} - -func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { - // v1alpha2 is hub, this is a no-op - return nil -} +// Hub marks v1alpha2 as the storage version for conversion. +// The Hub interface requires only this marker method. +// All conversion logic is implemented on the v1alpha1 spoke. +func (dst *LlamaStackDistribution) Hub() {} ``` #### 4.2 Implement v1alpha1 Spoke Conversion @@ -859,7 +1056,7 @@ func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: llamastackdistributions.llamastack.ai + name: llamastackdistributions.llamastack.io spec: conversion: strategy: Webhook diff --git a/specs/002-operator-generated-config/spec.md b/specs/002-operator-generated-config/spec.md index 62b3be220..f70d5c74f 100644 --- a/specs/002-operator-generated-config/spec.md +++ b/specs/002-operator-generated-config/spec.md @@ -110,6 +110,21 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat 2. **Given** a v1alpha1 CR, **When** I retrieve it as v1alpha2, **Then** the conversion webhook translates fields correctly 3. **Given** a v1alpha2 CR, **When** I retrieve it as v1alpha1, **Then** the conversion webhook translates fields correctly (where mappable) +### User Story 8 - Runtime Configuration Updates (Priority: P1) + +As a platform operator, I want to update the LLSD CR (e.g., add a provider, change storage) and have the running LlamaStack instance pick up the changes automatically, so that I can manage configuration declaratively without manual restarts. + +**Why this priority**: Day-2 operations are essential for production use. Without this, users must delete and recreate CRs to change configuration. + +**Independent Test**: Deploy a LLSD CR, wait for Ready, modify the CR's providers section, verify the Pod restarts with the updated config.yaml. + +**Acceptance Scenarios**: + +1. **Given** a running LLSD instance, **When** I add a new provider to `spec.providers`, **Then** the operator regenerates config.yaml, creates a new ConfigMap, and triggers a rolling update +2. **Given** a running LLSD instance, **When** I modify `spec.providers` but the generated config.yaml is identical (e.g., whitespace-only change), **Then** the operator does NOT restart the Pod +3. **Given** a running LLSD instance, **When** I update `spec.providers` with an invalid configuration, **Then** the operator preserves the current running config, reports the error in status, and does NOT disrupt the running instance +4. **Given** a running LLSD instance, **When** I change `spec.distribution.name` to a different distribution, **Then** the operator updates both the image and config atomically in a single Deployment update + ### Edge Cases - **Provider with settings escape hatch**: @@ -136,6 +151,30 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat - What: Distribution image has unsupported config.yaml version - Expected: Reconciliation fails with clear error about version incompatibility +- **CR update during active rollout**: + - What: User updates the CR while a previous config change is still rolling out + - Expected: Operator generates config from the latest CR spec, superseding the in-progress rollout. The Deployment converges to the newest desired state. + +- **Operator upgrade with running LLSD instances**: + - What: Operator is upgraded from v1 to v2, changing the image that `distribution.name: rh-dev` resolves to + - Expected: Operator detects the image change, regenerates config using the new base config matching the new image, and updates the Deployment atomically (new image + new config together). If config generation fails for the new image, the operator preserves the current running Deployment and reports the error in status. + +- **Config generation failure on update**: + - What: User changes CR in a way that produces an invalid merged config (e.g., references a provider type not supported by the distribution) + - Expected: Operator keeps the current running config and Deployment unchanged, sets `ConfigGenerated=False` with a descriptive error, and does not trigger a Pod restart + +- **Deeply nested secretKeyRef in settings**: + - What: User specifies `settings: {database: {connection: {secretKeyRef: {name: db, key: url}}}}` + - Expected: The nested secretKeyRef is NOT resolved as a secret reference. Only top-level settings values are inspected for secretKeyRef (e.g., `settings.host.secretKeyRef`). The deeply nested object is passed through to config.yaml as a literal map. + +- **Tools specified without toolRuntime provider**: + - What: User specifies `resources.tools: [websearch]` but does not configure `providers.toolRuntime` + - Expected: If the base config provides a default toolRuntime provider, tools are registered with it. If no toolRuntime provider exists in either user config or base config, validation fails with: "resources.tools requires at least one toolRuntime provider to be configured" + +- **Shields specified without safety provider**: + - What: User specifies `resources.shields: [llama-guard]` but does not configure `providers.safety` + - Expected: If the base config provides a default safety provider, shields are registered with it. If no safety provider exists in either user config or base config, validation fails with: "resources.shields requires at least one safety provider to be configured" + ## Requirements ### Functional Requirements @@ -146,7 +185,7 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat - **FR-002**: The `spec.distribution` field MUST support both `name` (mapped) and `image` (direct) forms, mutually exclusive - **FR-003**: The `spec.providers` section MUST support provider types: `inference`, `safety`, `vectorIo`, `toolRuntime`, `telemetry` - **FR-004**: Each provider MUST support polymorphic form: single object OR list of objects with explicit `id` field -- **FR-005**: Each provider MUST support fields: `provider` (type), `endpoint`, `apiKey` (secretKeyRef), `settings` (escape hatch) +- **FR-005**: Each provider MUST support fields: `provider` (type), `endpoint`, `apiKey` (secretKeyRef), `settings` (escape hatch). Provider-specific connection fields (e.g., `host` for vectorIo) MUST use `secretKeyRef` within `settings` rather than top-level named fields, keeping the provider schema uniform. The operator MUST recognize `secretKeyRef` objects only at the top level of `settings` values (i.e., `settings..secretKeyRef`), not at arbitrary nesting depth. Deeper nesting is passed through to config.yaml as-is without secret resolution. - **FR-006**: The `spec.resources` section MUST support: `models`, `tools`, `shields` - **FR-007**: Resources MUST support polymorphic form: simple string OR object with metadata - **FR-008**: The `spec.storage` section MUST have subsections: `kv` (key-value) and `sql` (relational) @@ -154,12 +193,15 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat - **FR-010**: The `spec.networking` section MUST consolidate: `port`, `tls`, `expose`, `allowedFrom` - **FR-011**: The `networking.expose` field MUST support polymorphic form: boolean OR object with `hostname` - **FR-012**: The `spec.workload` section MUST contain K8s deployment settings: `replicas`, `workers`, `resources`, `autoscaling`, `storage`, `podDisruptionBudget`, `topologySpreadConstraints`, `overrides` -- **FR-013**: The `spec.overrideConfig` field MUST be mutually exclusive with `providers`, `resources`, `storage`, `disabled` +- **FR-013**: The `spec.overrideConfig` field MUST be mutually exclusive with `providers`, `resources`, `storage`, `disabled`. The referenced ConfigMap MUST reside in the same namespace as the LLSD CR (consistent with namespace-scoped RBAC, constitution section 1.1) - **FR-014**: The `spec.externalProviders` field MUST remain for integration with spec 001 #### Configuration Generation -- **FR-020**: The operator MUST extract base config.yaml from the distribution container image +- **FR-020**: The operator MUST resolve `distribution.name` to a concrete image reference using the embedded distribution registry (`distributions.json`) and operator config overrides (`image-overrides`) +- **FR-020a**: The operator MUST record the resolved image reference in `status.resolvedDistribution.image` after successful resolution +- **FR-020b**: When the resolved image changes between reconciliations (e.g., after operator upgrade changes the `distributions.json` mapping), the operator MUST regenerate config.yaml using the base config matching the new image and update the Deployment atomically (image + config in a single update) +- **FR-020c**: The operator MUST NOT update a running LLSD's config without also updating its image to match. Image and base config MUST always be consistent. - **FR-021**: The operator MUST generate a complete config.yaml by merging user configuration over base defaults - **FR-022**: The operator MUST resolve `secretKeyRef` references to environment variables with deterministic naming - **FR-023**: The operator MUST create a ConfigMap containing the generated config.yaml @@ -170,36 +212,48 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat - **FR-028**: The operator MUST support config.yaml schema versions n and n-1 (current and previous) - **FR-029**: The operator MUST reject unsupported config.yaml versions with error: "Unsupported config.yaml version {version}. Supported versions: {list}" -#### Base Config Extraction from OCI Images +#### Base Config Extraction (Phased Approach) -- **FR-027a**: Distribution images SHOULD include `config.yaml` in OCI labels using one of: +The base config extraction follows a phased approach. Phase 1 provides an implementation that works without changes to distribution image build processes. Phase 2 adds OCI label-based extraction for distributions that support it. + +**Phase 1 - Embedded Default Configs (MVP)** + +- **FR-027a**: The operator MUST include embedded default configurations for all distribution names defined in `distributions.json`, shipped as part of the operator binary +- **FR-027b**: When `distribution.name` is specified, the operator MUST use the embedded config for that distribution as the base for config generation +- **FR-027c**: When `distribution.image` is specified (direct image reference, no named distribution), the operator MUST require `overrideConfig.configMapName` to provide the base configuration. If `overrideConfig` is not set and no OCI config labels are found (see Phase 2), the operator MUST set `ConfigGenerated=False` with reason `BaseConfigRequired` and message: "Direct image references require either overrideConfig.configMapName or OCI config labels on the image. See docs/configuration.md for details." +- **FR-027d**: The embedded configs MUST be versioned together with the distribution image mappings in `distributions.json`, ensuring each distribution name maps to a consistent (image, config) pair per operator release +- **FR-027e**: The operator MUST support air-gapped environments where images are mirrored to internal registries. The embedded config is used regardless of where the image is pulled from. + +**Phase 2 - OCI Label Extraction (Enhancement)** + +- **FR-027f**: Distribution images MAY include `config.yaml` in OCI labels using one of: - `io.llamastack.config.base64`: Base64-encoded config (for configs < 50KB) - `io.llamastack.config.layer` + `io.llamastack.config.path`: Layer digest and path reference (for larger configs) -- **FR-027b**: The operator MUST extract base config from OCI labels in priority order: `base64` โ†’ `layer reference` -- **FR-027c**: The operator MUST use `k8schain` authentication to access registries using the same credentials as kubelet (imagePullSecrets, ServiceAccount) -- **FR-027d**: The operator MUST cache extracted configs by image digest to avoid repeated registry fetches -- **FR-027e**: When distribution image lacks config labels, the operator MUST: - 1. Set status condition `ConfigGenerated=False` with reason `MissingConfigLabels` - 2. Return error: "Distribution image {image} missing config labels. Add labels using `crane mutate` or use `overrideConfig` to provide configuration manually. See docs/configuration.md for details." -- **FR-027f**: The operator MUST support air-gapped environments where images are mirrored to internal registries -- **FR-027g**: When OCI labels are missing, users MAY use `overrideConfig.configMapName` as a workaround to provide full configuration manually +- **FR-027g**: When OCI config labels are present on the resolved image, the operator MUST use the label-provided config as the base, taking precedence over embedded defaults +- **FR-027h**: The operator MUST use `k8schain` authentication to access registries using the same credentials as kubelet (imagePullSecrets, ServiceAccount) +- **FR-027i**: The operator MUST cache extracted configs by image digest to avoid repeated registry fetches +- **FR-027j**: OCI label extraction enables `distribution.image` usage without `overrideConfig`, since the image itself provides the base config #### Provider Configuration - **FR-030**: Provider `provider` field MUST map to `provider_type` with `remote::` prefix (e.g., `vllm` becomes `remote::vllm`) - **FR-031**: Provider `endpoint` field MUST map to `config.url` in config.yaml -- **FR-032**: Provider `apiKey.secretKeyRef` MUST be resolved to an environment variable and referenced as `${env.LLSD__API_KEY}` +- **FR-032**: Provider `apiKey.secretKeyRef` MUST be resolved to an environment variable and referenced as `${env.LLSD__}`, where `` is the provider's unique `id` (explicit or auto-generated per FR-035), uppercased with hyphens replaced by underscores. Example: provider ID `vllm-primary` with field `apiKey` produces `LLSD_VLLM_PRIMARY_API_KEY`. - **FR-033**: Provider `settings` MUST be merged into the provider's `config` section in config.yaml - **FR-034**: When multiple providers are specified, each MUST have an explicit `id` field - **FR-035**: Single provider without `id` MUST auto-generate `provider_id` from the `provider` field value +#### Telemetry Provider + +- **FR-036**: Telemetry providers follow the same schema as other provider types (FR-004, FR-005). The `provider` field maps to the telemetry backend (e.g., `opentelemetry`). The `endpoint` and `settings` fields configure the telemetry destination. No telemetry-specific fields are defined beyond the standard provider schema. + #### Resource Registration - **FR-040**: Simple model strings MUST be registered with the inference provider. When multiple inference providers are configured (list form), the first provider in list order is used. When a single provider is configured (object form), that provider is used. - **FR-041**: Model objects with explicit `provider` MUST be registered with the specified provider - **FR-042**: Model objects MAY include metadata fields: `contextLength`, `modelType`, `quantization` -- **FR-043**: Tools MUST be registered as tool groups with default provider configuration -- **FR-044**: Shields MUST be registered with the configured safety provider +- **FR-043**: Tools MUST be registered as tool groups with the configured toolRuntime provider. When no explicit toolRuntime provider is configured in the CR, the base config's toolRuntime provider is used. If no toolRuntime provider exists in either source, controller validation MUST fail with an actionable error +- **FR-044**: Shields MUST be registered with the configured safety provider. When no explicit safety provider is configured in the CR, the base config's safety provider is used. If no safety provider exists in either source, controller validation MUST fail with an actionable error #### Storage Configuration @@ -223,12 +277,15 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat #### Validation -- **FR-070**: CEL validation MUST enforce mutual exclusivity between `providers` and `overrideConfig` +- **FR-070**: CEL validation MUST enforce mutual exclusivity between `overrideConfig` and each of `providers`, `resources`, `storage`, and `disabled` - **FR-071**: CEL validation MUST require explicit `id` when multiple providers are specified for the same API - **FR-072**: CEL validation MUST enforce unique provider IDs across all provider types - **FR-073**: Controller validation MUST verify referenced Secrets exist before generating config - **FR-074**: Controller validation MUST verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` - **FR-075**: Validation errors MUST include actionable messages with field paths +- **FR-076**: A validating admission webhook MUST validate CR creation and update operations for constraints that cannot be expressed in CEL (e.g., Secret existence checks, ConfigMap existence for `overrideConfig`, cross-field semantic validation such as provider ID references in resources) +- **FR-077**: The validating webhook MUST return structured error responses with field paths and actionable messages following Kubernetes API conventions +- **FR-078**: The validating webhook MUST be deployed as part of the operator installation and configured via the operator's kustomize manifests with appropriate certificate management #### API Version Conversion @@ -243,6 +300,19 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat - **FR-091**: External providers (from spec 001) MUST be additive to inline providers - **FR-092**: When external provider ID conflicts with inline provider, external MUST override with warning +#### Runtime Configuration Updates + +- **FR-095**: When the CR `spec` changes, the operator MUST regenerate config.yaml, create a new ConfigMap with an updated content hash, and update the Deployment to reference the new ConfigMap +- **FR-096**: The operator MUST NOT restart Pods if the generated config.yaml content is identical to the currently deployed config (content hash comparison) +- **FR-097**: If config generation or validation fails during a CR update, the operator MUST preserve the current running Deployment (image, ConfigMap, env vars) unchanged and set status condition `ConfigGenerated=False` with the failure reason. The running instance MUST NOT be disrupted. +- **FR-098**: When `spec.distribution` changes (name or image), the operator MUST update the Deployment atomically per FR-100 +- **FR-099**: Status conditions MUST reflect the reconciliation state during updates: `ConfigGenerated` (config.yaml created successfully), `DeploymentUpdated` (Deployment spec updated), `Available` (at least one Pod is ready with the current config) + +#### Distribution Lifecycle + +- **FR-100**: The operator MUST update the Deployment's container image and its generated config atomically in a single Deployment update. There MUST be no intermediate state where the running image and config are mismatched. +- **FR-101**: When config generation fails after an operator upgrade (e.g., the new base config is incompatible with the user's CR fields), the operator MUST preserve the running Deployment per FR-097 and report the failure with reason `UpgradeConfigFailure` + ### Non-Functional Requirements - **NFR-001**: Configuration generation MUST be deterministic (same inputs produce same outputs) @@ -254,9 +324,20 @@ As an existing user, I want my v1alpha1 CRs to continue working after the operat ### External Dependencies -#### Distribution Image Build Requirements +#### Operator Build Requirements (Phase 1) + +The operator binary MUST embed default configs for all named distributions: + +| Artifact | Description | Required | +|----------|-------------|----------| +| `distributions.json` | Maps distribution names to image references | Yes | +| `configs//config.yaml` | Default config.yaml for each named distribution | Yes | + +The `distributions.json` and corresponding config files are maintained together and updated as part of the operator release process. For downstream builds (e.g., RHOAI), the `image-overrides` mechanism in the operator ConfigMap allows overriding the embedded image references without rebuilding the operator. -Distribution images must include OCI labels for base config extraction. These labels are added post-build using `crane mutate`: +#### Distribution Image Build Requirements (Phase 2) + +Distribution images MAY include OCI labels for base config extraction. These labels are added post-build using `crane mutate`: | Label | Description | Required | |-------|-------------|----------| @@ -279,6 +360,8 @@ crane mutate ${IMAGE}:build \ **Why post-build**: Labels containing layer digests cannot be added during build (the layer digest is only known after build). `crane mutate` solves this by updating only the config blob without modifying layers. +**Adoption path**: OCI labels are optional in Phase 1. Distributions that adopt labels gain the ability to be used with `distribution.image` without `overrideConfig`. A future "LLS Distribution Image Specification" document will formalize the label contract. + ### Key Entities - **ProviderSpec**: Configuration for a single provider (inference, safety, vectorIo, etc.) @@ -287,13 +370,14 @@ crane mutate ${IMAGE}:build \ - **NetworkingSpec**: Configuration for network exposure (port, TLS, expose, allowedFrom) - **WorkloadSpec**: Kubernetes deployment settings (replicas, resources, autoscaling) - **ExposeConfig**: Polymorphic expose configuration (bool or object with hostname) +- **ResolvedDistributionStatus**: Tracks the resolved image reference, config source (embedded/oci-label), and config hash for change detection ## CRD Schema ### Complete v1alpha2 Spec Structure ```yaml -apiVersion: llamastack.ai/v1alpha2 +apiVersion: llamastack.io/v1alpha2 kind: LlamaStackDistribution metadata: name: my-stack @@ -303,18 +387,20 @@ spec: providers: inference: - provider: vllm - endpoint: "http://vllm:8000" - apiKey: - secretKeyRef: {name: vllm-creds, key: token} - settings: - max_tokens: 8192 + - id: vllm-primary + provider: vllm + endpoint: "http://vllm:8000" + apiKey: + secretKeyRef: {name: vllm-creds, key: token} + settings: + max_tokens: 8192 safety: provider: llama-guard vectorIo: provider: pgvector - host: - secretKeyRef: {name: pg-creds, key: host} + settings: + host: + secretKeyRef: {name: pg-creds, key: host} resources: models: @@ -362,7 +448,7 @@ spec: autoscaling: minReplicas: 1 maxReplicas: 5 - targetCPUUtilization: 80 + targetCPUUtilizationPercentage: 80 storage: size: "10Gi" mountPath: "/.llama" @@ -422,47 +508,68 @@ spec: 1. Fetch LLSD CR โ”‚ โ–ผ -2. Validate Configuration +2. Resolve Distribution + โ”œโ”€โ”€ distribution.name โ†’ lookup in distributions.json + โ”‚ โ””โ”€โ”€ Check image-overrides in operator ConfigMap + โ”œโ”€โ”€ distribution.image โ†’ use directly + โ””โ”€โ”€ Record resolved image in status.resolvedDistribution + โ”‚ + โ–ผ +3. Validate Configuration (webhook + controller) โ”œโ”€โ”€ Check mutual exclusivity (providers vs overrideConfig) โ”œโ”€โ”€ Validate secret references exist - โ””โ”€โ”€ Validate ConfigMap references exist + โ”œโ”€โ”€ Validate ConfigMap references exist + โ””โ”€โ”€ Validate provider ID references in resources โ”‚ โ–ผ -3. Determine Config Source +4. Determine Config Source โ”œโ”€โ”€ If overrideConfig: Use referenced ConfigMap directly โ””โ”€โ”€ If providers/resources: Generate config โ”‚ โ–ผ -4. Generate Configuration (if not using overrideConfig) - โ”œโ”€โ”€ Extract base config.yaml from distribution image - โ”œโ”€โ”€ Expand providers to full config.yaml format +5. Obtain Base Config + โ”œโ”€โ”€ Check OCI labels on resolved image (Phase 2) + โ”œโ”€โ”€ Fall back to embedded config for distribution name (Phase 1) + โ””โ”€โ”€ Require overrideConfig if neither is available + โ”‚ + โ–ผ +6. Generate Configuration (if not using overrideConfig) + โ”œโ”€โ”€ Merge user providers over base config providers โ”œโ”€โ”€ Expand resources to registered_resources format โ”œโ”€โ”€ Apply storage configuration โ”œโ”€โ”€ Apply disabled APIs - โ””โ”€โ”€ Resolve secretKeyRef to environment variables + โ”œโ”€โ”€ Resolve secretKeyRef to environment variables + โ””โ”€โ”€ Validate generated config structure โ”‚ โ–ผ -5. Create/Update ConfigMap - โ”œโ”€โ”€ Generate ConfigMap with content hash in name - โ”œโ”€โ”€ Set owner reference - โ””โ”€โ”€ Create new ConfigMap (immutable pattern) +7. Merge External Providers (from spec 001) + โ”œโ”€โ”€ Add external providers to generated config + โ””โ”€โ”€ Override on ID conflict (with warning) โ”‚ โ–ผ -6. Update Deployment - โ”œโ”€โ”€ Mount generated ConfigMap - โ”œโ”€โ”€ Inject environment variables for secrets - โ””โ”€โ”€ Add hash annotation for rollout trigger +8. Compare with Current State + โ”œโ”€โ”€ Hash merged config against current ConfigMap + โ”œโ”€โ”€ If identical โ†’ skip update, no Pod restart + โ””โ”€โ”€ If different โ†’ proceed to update โ”‚ โ–ผ -7. Merge External Providers (from spec 001) - โ”œโ”€โ”€ Add external providers to config - โ””โ”€โ”€ Override on ID conflict (with warning) +9. Create ConfigMap + Update Deployment (atomic) + โ”œโ”€โ”€ Create new ConfigMap with content hash in name + โ”œโ”€โ”€ Set owner reference on ConfigMap + โ”œโ”€โ”€ Update Deployment atomically: + โ”‚ โ”œโ”€โ”€ Container image (from resolved distribution) + โ”‚ โ”œโ”€โ”€ ConfigMap volume mount + โ”‚ โ”œโ”€โ”€ Environment variables for secrets + โ”‚ โ””โ”€โ”€ Hash annotation for rollout trigger + โ””โ”€โ”€ On failure โ†’ preserve current Deployment, report error โ”‚ โ–ผ -8. Update Status - โ”œโ”€โ”€ Set phase - โ”œโ”€โ”€ Update conditions - โ””โ”€โ”€ Record config generation details +10. Update Status + โ”œโ”€โ”€ Set phase + โ”œโ”€โ”€ Update conditions (ConfigGenerated, DeploymentUpdated, + โ”‚ Available) + โ”œโ”€โ”€ Record resolvedDistribution (image, configHash) + โ””โ”€โ”€ Record config generation details ``` ## Configuration Tiers @@ -475,6 +582,30 @@ spec: ## Status Reporting +### Printer Columns + +The v1alpha2 CRD MUST define the following printer columns for `kubectl get` output (constitution ยง2.5): + +```go +//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Distribution",type="string",JSONPath=".status.resolvedDistribution.image",priority=1 +//+kubebuilder:printcolumn:name="Config",type="string",JSONPath=".status.configGeneration.configMapName",priority=1 +//+kubebuilder:printcolumn:name="Providers",type="integer",JSONPath=".status.configGeneration.providerCount" +//+kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +``` + +Default `kubectl get llsd` output: + +``` +NAME PHASE PROVIDERS AVAILABLE AGE +my-stack Ready 3 1 5m +``` + +Wide output (`kubectl get llsd -o wide`) includes `priority=1` columns (Distribution, Config). + +### Status Fields + The status MUST include: ```yaml @@ -485,10 +616,22 @@ status: status: "True" reason: ConfigGenerationSucceeded message: "Generated config.yaml with 3 providers and 2 models" + - type: DeploymentUpdated + status: "True" + reason: DeploymentUpdateSucceeded + message: "Deployment updated with config my-stack-config-abc123" + - type: Available + status: "True" + reason: MinimumReplicasAvailable + message: "1/1 replicas available with current config" - type: SecretsResolved status: "True" reason: AllSecretsFound message: "Resolved 2 secret references" + resolvedDistribution: + image: "docker.io/llamastack/distribution-starter@sha256:abc123" + configSource: embedded # or "oci-label" (Phase 2) + configHash: "sha256:def456" configGeneration: configMapName: my-stack-config-abc123 generatedAt: "2026-02-02T12:00:00Z" @@ -499,9 +642,17 @@ status: ## Security Considerations - **Secret Handling**: Secret values MUST only be passed via environment variables, never embedded in ConfigMap -- **Environment Variable Naming**: Use deterministic, prefixed names: `LLSD__` (e.g., `LLSD_INFERENCE_API_KEY`) +- **Environment Variable Naming**: Use deterministic, prefixed names: `LLSD__` (e.g., `LLSD_VLLM_PRIMARY_API_KEY`). Provider ID is uppercased with hyphens replaced by underscores. - **ConfigMap Permissions**: Generated ConfigMaps inherit namespace RBAC - **Image Extraction**: Config extraction from images uses read-only operations +- **Webhook Permissions**: The `ValidatingWebhookConfiguration` is a cluster-scoped resource, installed by OLM or kustomize during operator setup (not by the operator at runtime). This is a standard pattern for Kubernetes operators with admission webhooks and is an accepted deviation from constitution ยง1.1. The operator itself remains namespace-scoped at runtime. + +## Open Questions + +- ~~**OQ-001**~~: Resolved. `expose: {}` is treated as `expose: true` (see Edge Cases). +- ~~**OQ-002**~~: Resolved. Disabled API + provider config conflict produces a **warning** (not an error). The `disabled` list takes precedence: the provider config is accepted but ignored at runtime. Warning is logged and reported in status conditions. (From PR #242 review; resolved per edge case "Disabled APIs conflict with providers") +- ~~**OQ-003**~~: Resolved. Environment variable naming uses the **provider ID** (not provider type or API type): `LLSD__`. The provider ID is unique across all providers (enforced by FR-072), ensuring no collisions. For single providers without explicit `id`, the auto-generated ID from FR-035 is used. Examples: `LLSD_VLLM_PRIMARY_API_KEY`, `LLSD_PGVECTOR_HOST`. Characters not valid in env var names (hyphens) are replaced with underscores and uppercased. (From PR #242 review) +- **OQ-004**: Should the operator create a default LlamaStackDistribution instance when installed? This is uncommon for Kubernetes operators but could improve the getting-started experience. If adopted, it should be opt-in via operator configuration (e.g., a Helm value or OLM parameter). (From team discussion, 2026-02-10) ## References diff --git a/specs/002-operator-generated-config/tasks.md b/specs/002-operator-generated-config/tasks.md index cad544b56..873e06a13 100644 --- a/specs/002-operator-generated-config/tasks.md +++ b/specs/002-operator-generated-config/tasks.md @@ -5,13 +5,13 @@ ## Task Overview -| Phase | Tasks | Priority | Estimated Effort | -|-------|-------|----------|------------------| -| Phase 1: CRD Schema | 8 tasks | P1 | Medium | -| Phase 2: Config Generation | 8 tasks | P1 | Large | -| Phase 3: Controller Integration | 8 tasks | P1 | Large | -| Phase 4: Conversion Webhook | 4 tasks | P2 | Medium | -| Phase 5: Testing & Docs | 5 tasks | P2 | Medium | +| Phase | Tasks | Priority | +|-------|-------|----------| +| Phase 1: CRD Schema | 8 tasks | P1 | +| Phase 2: Config Generation | 8 tasks | P1 | +| Phase 3: Controller Integration | 12 tasks | P1 | +| Phase 4: Conversion Webhook | 4 tasks | P2 | +| Phase 5: Testing & Docs | 6 tasks | P2 | --- @@ -31,7 +31,7 @@ Create the v1alpha2 API package with groupversion_info.go and base types. **Acceptance criteria**: - [ ] v1alpha2 package compiles -- [ ] GroupVersion is `llamastack.ai/v1alpha2` +- [ ] GroupVersion is `llamastack.io/v1alpha2` - [ ] Scheme registration works --- @@ -46,7 +46,7 @@ Define ProvidersSpec and ProviderConfig types with polymorphic support. **Types to define**: - `ProvidersSpec` (inference, safety, vectorIo, toolRuntime, telemetry) -- `ProviderConfig` (id, provider, endpoint, apiKey, host, settings) +- `ProviderConfig` (id, provider, endpoint, apiKey, settings) - `ProviderConfigOrList` (polymorphic wrapper using json.RawMessage) - `SecretKeyRef` (name, key) @@ -161,19 +161,28 @@ Create the main LlamaStackDistributionSpec with all sections and add CEL validat - `LlamaStackDistributionSpec` (distribution, providers, resources, storage, disabled, networking, workload, externalProviders, overrideConfig) - `LlamaStackDistribution` (main CRD type) - `LlamaStackDistributionList` -- `OverrideConfigSpec` (configMapName, configMapNamespace) +- `OverrideConfigSpec` (configMapName; must be in same namespace as CR) **CEL validations**: - Mutual exclusivity: providers vs overrideConfig - Mutual exclusivity: resources vs overrideConfig - Mutual exclusivity: storage vs overrideConfig +- Mutual exclusivity: disabled vs overrideConfig **Requirements covered**: FR-001, FR-002, FR-009, FR-013, FR-014, FR-070 +**Printer columns** (constitution ยง2.5): +- `Phase` (`.status.phase`) +- `Distribution` (`.status.resolvedDistribution.image`, priority=1, wide output only) +- `Config` (`.status.configGeneration.configMapName`, priority=1, wide output only) +- `Providers` (`.status.configGeneration.providerCount`) +- `Available` (`.status.availableReplicas`) +- `Age` (`.metadata.creationTimestamp`) + **Acceptance criteria**: - [ ] Complete spec structure compiles - [ ] CEL validations reject invalid combinations -- [ ] Printer columns defined for kubectl output +- [ ] Printer columns defined: Phase, Providers, Available, Age (default); Distribution, Config (wide) --- @@ -220,63 +229,47 @@ Create the pkg/config package directory structure with basic types. --- -### Task 2.2: Implement Base Config Extraction (OCI Label Approach) +### Task 2.2: Implement Base Config Resolution (Phased) **Priority**: P1 **Blocked by**: 2.1 **Description**: -Implement extraction of base config.yaml from distribution images using OCI labels. -Uses `k8schain` for registry authentication (same credentials as kubelet). - -**File**: `pkg/config/extractor.go` - -**Approach**: -1. Fetch image config blob (contains labels) using `crane.Config()` -2. Check for `io.llamastack.config.base64` label (inline config) -3. If not present, check for `io.llamastack.config.layer` + `.path` (layer reference) -4. Extract config from appropriate source -5. Cache by image digest - -**Dependencies**: -- `github.com/google/go-containerregistry/pkg/crane` -- `github.com/google/go-containerregistry/pkg/authn/k8schain` - -**Types**: -```go -type ConfigLocation struct { - Base64 string // Inline base64 encoded config - LayerDigest string // Layer digest containing config - Path string // Path within layer - Version string // Config schema version -} +Implement base config resolution with a phased approach. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override. -type ImageConfigExtractor struct { - k8sClient client.Client - namespace string - serviceAccount string - cache *sync.Map // digest -> BaseConfig -} -``` +**Files**: +- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic +- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) +- `Makefile` - Build-time validation target (`validate-configs`) + +**Phase 1 (MVP) Approach**: +1. Create `configs//config.yaml` for each distribution in `distributions.json` +2. Embed via `//go:embed configs` in the resolver package +3. On resolution: lookup embedded config by `distribution.name` +4. For `distribution.image` without OCI labels: require `overrideConfig` + +**Phase 2 (Enhancement) Approach**: +1. Add `pkg/config/oci_extractor.go` using `k8schain` for registry auth +2. Check OCI labels on resolved image first (takes precedence over embedded) +3. Fall back to embedded config if no labels found +4. Cache by image digest **Functions**: -- `NewImageConfigExtractor(client, namespace, sa) *ImageConfigExtractor` -- `(e *ImageConfigExtractor) Extract(ctx, imageRef) (*BaseConfig, error)` -- `(e *ImageConfigExtractor) getConfigLocation(ctx, imageRef, keychain) (*ConfigLocation, error)` -- `(e *ImageConfigExtractor) extractFromBase64(b64) (*BaseConfig, error)` -- `(e *ImageConfigExtractor) extractFromLayer(ctx, imageRef, layerDigest, path, keychain) (*BaseConfig, error)` +- `NewBaseConfigResolver(distributionImages, imageOverrides) *BaseConfigResolver` +- `(r *BaseConfigResolver) Resolve(ctx, distribution) (*BaseConfig, string, error)` +- `(r *BaseConfigResolver) loadEmbeddedConfig(name) (*BaseConfig, error)` +- `(r *BaseConfigResolver) resolveImage(distribution) (string, error)` -**Requirements covered**: FR-020, FR-027a through FR-027f, NFR-006 - -**Alternative**: See `alternatives/init-container-extraction.md` for init container approach +**Requirements covered**: FR-020, FR-027a through FR-027e (Phase 1), FR-027f through FR-027j (Phase 2), NFR-006 **Acceptance criteria**: -- [ ] Can extract config from `io.llamastack.config.base64` label -- [ ] Can extract config from layer using `io.llamastack.config.layer` + `.path` labels -- [ ] Uses k8schain for registry authentication (respects imagePullSecrets) -- [ ] Caching by image digest prevents repeated extraction -- [ ] Clear error message when distribution image lacks config labels -- [ ] Unit tests for both extraction strategies +- [ ] Embedded configs loaded via `go:embed` for all named distributions +- [ ] `distribution.name` resolves to embedded config +- [ ] `distribution.image` without OCI labels returns clear error requiring `overrideConfig` +- [ ] Build-time validation ensures all distributions have configs +- [ ] (Phase 2) OCI label extraction takes precedence over embedded when available +- [ ] (Phase 2) Caching by image digest prevents repeated extraction +- [ ] Unit tests for resolution priority logic --- @@ -352,6 +345,8 @@ Implement resource spec expansion to registered_resources format. - [ ] Model objects expand correctly - [ ] Default provider assignment works - [ ] Tools and shields expand correctly +- [ ] Tools fail with actionable error when no toolRuntime provider exists (user or base config) +- [ ] Shields fail with actionable error when no safety provider exists (user or base config) --- @@ -641,8 +636,10 @@ Add config generation status fields and conditions. **File**: `controllers/status.go` **New conditions**: -- `ConfigGenerated` -- `SecretsResolved` +- `ConfigGenerated`: True when config successfully generated +- `DeploymentUpdated`: True when Deployment spec updated with current config +- `Available`: True when at least one Pod is ready with current config +- `SecretsResolved`: True when all secret references valid **New status fields**: ```go @@ -653,15 +650,190 @@ type ConfigGenerationStatus struct { ResourceCount int ConfigVersion int } + +type ResolvedDistributionStatus struct { + Image string // Resolved image from distribution.name + ConfigSource string // "embedded" or "oci-label" + ConfigHash string // Hash of current base config +} ``` +**Requirements covered**: FR-020a, FR-099 + **Acceptance criteria**: - [ ] New conditions set correctly - [ ] Config generation details in status +- [ ] `resolvedDistribution` recorded in status - [ ] Status updated on each reconcile --- +### Task 3.9: Implement Validating Admission Webhook + +**Priority**: P1 +**Blocked by**: 1.7 + +**Description**: +Implement a validating webhook for constraints that cannot be expressed in CEL, complementing the CEL rules added in Phase 1 (Task 1.7). + +**File**: `api/v1alpha2/llamastackdistribution_webhook.go` + +**Validation logic**: +- Verify referenced Secrets exist in the namespace (fast admission-time feedback) +- Verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` +- Validate provider ID references in `resources.models[].provider` +- Cross-field semantic validation (e.g., model provider references valid provider IDs) + +**Configuration**: +- Webhook deployment via kustomize manifests (`config/webhook/`) +- Certificate management using cert-manager or operator-managed self-signed certs +- Failure policy: `Fail` (reject CR if webhook unreachable) + +```go +func (r *LlamaStackDistribution) ValidateCreate() (admission.Warnings, error) { + return r.validate() +} + +func (r *LlamaStackDistribution) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + return r.validate() +} + +func (r *LlamaStackDistribution) validate() (admission.Warnings, error) { + var allErrs field.ErrorList + // Validate secret references exist + // Validate ConfigMap references exist + // Validate provider ID cross-references + return nil, allErrs.ToAggregate() +} +``` + +**Requirements covered**: FR-076, FR-077, FR-078 + +**Acceptance criteria**: +- [ ] Webhook validates Secret existence at admission time +- [ ] Webhook validates ConfigMap references +- [ ] Webhook validates cross-field provider ID references +- [ ] Clear error messages with field paths +- [ ] Webhook kustomize manifests configured + +--- + +### Task 3.10: Implement Runtime Configuration Update Logic + +**Priority**: P1 +**Blocked by**: 3.3 + +**Description**: +On every reconciliation, compare the generated config hash with the currently deployed config hash. Only update the Deployment when content actually changes. On failure, preserve the current running Deployment. + +**File**: `controllers/llamastackdistribution_controller.go` + +**Logic**: +``` +Reconcile() +โ”œโ”€โ”€ Generate config (or use overrideConfig) +โ”œโ”€โ”€ Compute content hash of generated config +โ”œโ”€โ”€ Compare with status.configGeneration.configMapName hash +โ”œโ”€โ”€ If identical โ†’ skip update, no Pod restart +โ””โ”€โ”€ If different: + โ”œโ”€โ”€ Create new ConfigMap + โ”œโ”€โ”€ Update Deployment atomically (image + config + env) + โ”œโ”€โ”€ On success โ†’ update status + โ””โ”€โ”€ On failure โ†’ preserve current Deployment, report error +``` + +**Failure preservation**: +```go +func (r *Reconciler) reconcileConfig(ctx context.Context, instance *v1alpha2.LlamaStackDistribution) error { + generated, err := config.GenerateConfig(ctx, instance.Spec, resolvedImage) + if err != nil { + // Preserve current running state, report error + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "ConfigGenerated", + Status: metav1.ConditionFalse, + Reason: "ConfigGenerationFailed", + Message: err.Error(), + }) + return nil // Don't requeue, let user fix the CR + } + // ... proceed with update +} +``` + +**Requirements covered**: FR-095, FR-096, FR-097 + +**Acceptance criteria**: +- [ ] Config hash comparison prevents unnecessary restarts +- [ ] Failed config generation preserves current Deployment +- [ ] Error reported in status conditions on failure +- [ ] Successful update reflected in status + +--- + +### Task 3.11: Implement Atomic Deployment Updates + +**Priority**: P1 +**Blocked by**: 3.10 + +**Description**: +When the Deployment needs updating, apply all changes (image, ConfigMap mount, env vars, hash annotation) in a single `client.Update()` call to prevent intermediate states where the running image and config are mismatched. + +**File**: `controllers/llamastackdistribution_controller.go` + +```go +func (r *Reconciler) updateDeploymentAtomically( + ctx context.Context, + deployment *appsv1.Deployment, + resolvedImage string, + configMapName string, + envVars []corev1.EnvVar, + configHash string, +) error { + // Update all fields in one mutation + deployment.Spec.Template.Spec.Containers[0].Image = resolvedImage + // ... update ConfigMap volume, env vars, hash annotation + return r.Client.Update(ctx, deployment) +} +``` + +**Operator upgrade handling**: When the operator is upgraded and `distributions.json` maps a name to a new image, the reconciler detects the image change via `status.resolvedDistribution.image` comparison and triggers an atomic update with the new base config. + +**Requirements covered**: FR-098, FR-100, FR-101 + +**Acceptance criteria**: +- [ ] Image + ConfigMap + env vars updated in single API call +- [ ] No intermediate state where image and config mismatch +- [ ] Operator upgrade triggers atomic update when image changes +- [ ] Failed update preserves current Deployment (see FR-097) + +--- + +### Task 3.12: Implement Distribution Resolution Tracking + +**Priority**: P1 +**Blocked by**: 3.3 + +**Description**: +Track the resolved image in `status.resolvedDistribution` so the controller can detect changes across reconciliations (e.g., after operator upgrade where `distributions.json` maps a name to a new image). + +**File**: `controllers/llamastackdistribution_controller.go` + +**Logic**: +1. Resolve `distribution.name` to concrete image using `distributions.json` + `image-overrides` +2. Compare with `status.resolvedDistribution.image` +3. If different: regenerate config with new base, update atomically +4. Record new resolved image in status + +**Requirements covered**: FR-020a, FR-020b, FR-020c + +**Acceptance criteria**: +- [ ] Resolved image recorded in `status.resolvedDistribution` +- [ ] Image change detected between reconciliations +- [ ] Image change triggers config regeneration and atomic update +- [ ] Config source ("embedded" or "oci-label") recorded in status + +--- + ## Phase 4: Conversion Webhook ### Task 4.1: Implement v1alpha2 Hub @@ -675,21 +847,19 @@ Mark v1alpha2 as the conversion hub (storage version). **File**: `api/v1alpha2/llamastackdistribution_conversion.go` **Implementation**: -```go -func (src *LlamaStackDistribution) ConvertTo(dstRaw conversion.Hub) error { - return nil // v1alpha2 is hub -} -func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { - return nil // v1alpha2 is hub -} +In controller-runtime, the Hub only implements a marker method. Conversion logic lives on the Spoke (v1alpha1). + +```go +// Hub marks v1alpha2 as the storage version for conversion. +func (dst *LlamaStackDistribution) Hub() {} ``` **Requirements covered**: FR-081 **Acceptance criteria**: -- [ ] v1alpha2 implements Hub interface -- [ ] No-op conversion for hub +- [ ] v1alpha2 implements `conversion.Hub` interface via `Hub()` marker method +- [ ] No conversion logic on the hub (all conversion is on the v1alpha1 spoke) --- @@ -811,11 +981,18 @@ Write integration tests for v1alpha2 controller logic. - Network exposure - Override config - Validation errors +- Runtime config updates (US8): CR update triggers config regeneration +- Atomic Deployment updates: image + config updated together +- Webhook validation: invalid references rejected at admission +- Distribution resolution tracking: operator upgrade triggers update +- Config generation failure: current Deployment preserved **Acceptance criteria**: -- [ ] All user stories have tests +- [ ] All user stories have tests (including US8) - [ ] Edge cases covered - [ ] Error scenarios tested +- [ ] Webhook validation tested +- [ ] Atomic update scenarios tested --- @@ -884,6 +1061,30 @@ Update documentation for v1alpha2. --- +### Task 5.6: Performance Benchmarks + +**Priority**: P2 +**Blocked by**: 2.8 + +**Description**: +Write Go benchmark tests to verify config generation completes within the NFR-002 threshold (5 seconds for typical configurations). + +**File**: `pkg/config/config_benchmark_test.go` + +**Benchmark scenarios**: +- Single provider, single model (minimal config) +- 5 providers, 10 models, storage, networking (typical production) +- 10 providers, 50 models, all features enabled (stress test) + +**Requirements covered**: NFR-002 + +**Acceptance criteria**: +- [ ] Benchmark tests pass under 5 seconds for typical configuration +- [ ] Results documented in test output +- [ ] CI runs benchmarks (optional, for regression detection) + +--- + ## Task Dependencies Graph ``` @@ -905,12 +1106,18 @@ Phase 2 (Config Generation) Phase 3 (Controller) โ”œโ”€โ”€ 3.1 โ”€โ–บ 3.2, 3.5, 3.6 โ”œโ”€โ”€ 3.2 โ”€โ–บ 3.3 -โ”œโ”€โ”€ 3.3 โ”€โ–บ 3.4, 3.7, 3.8 -โ””โ”€โ”€ ... +โ”œโ”€โ”€ 3.3 โ”€โ–บ 3.4, 3.7, 3.8, 3.10, 3.12 +โ”œโ”€โ”€ 3.10 โ”€โ–บ 3.11 +โ”œโ”€โ”€ 3.9 (blocked by 1.7) +โ””โ”€โ”€ 3.12 (parallel with 3.10) Phase 4 (Webhook) โ””โ”€โ”€ 4.1 โ”€โ–บ 4.2, 4.3 โ”€โ–บ 4.4 Phase 5 (Testing) -โ””โ”€โ”€ Depends on respective phases +โ”œโ”€โ”€ 5.1 (blocked by Phase 2) +โ”œโ”€โ”€ 5.2 (blocked by Phase 3) +โ”œโ”€โ”€ 5.3 (blocked by Phase 4) +โ”œโ”€โ”€ 5.4, 5.5 (blocked by Phase 3, 4) +โ””โ”€โ”€ 5.6 (blocked by 2.8) ``` From 81fc0f3521c8289eda6022f6c08ea7f3a065dedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Mar 2026 07:23:26 +0100 Subject: [PATCH 5/9] docs(spec): revise v1alpha2 spec addressing PR #253 review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes to the v1alpha2 operator-generated config specification: - Replace polymorphic JSON types with typed []ProviderConfig slices, enabling kubebuilder validation and CEL rules (FR-004) - Add explicit secretRefs field on ProviderConfig, eliminating heuristic secret detection that caused false positives (FR-005) - Fix CEL validation feasibility: FR-071/FR-072 now work because providers are typed slices, not opaque JSON - Add missing CEL rules for TLS, Redis, and Postgres conditional field requirements (FR-079, FR-079a-c) - Add webhook validation for distribution name (FR-079d) - Change disabled+provider conflict from warning to error (OQ-002) - Clarify merge semantics with concrete before/after examples in config-generation contract - Add ConfigMap cleanup requirement (FR-025a, retain last 2) - Add ConfigResolver interface for Phase 2 extensibility (FR-027a1) - Add Kubernetes Events requirement (NFR-007) - Update all YAML examples across spec, quickstart, and contracts - Add 30-minute review recipe in review_summary.md Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../contracts/config-generation.yaml | 56 +- .../contracts/crd-schema.yaml | 43 +- .../data-model.md | 78 +- specs/002-operator-generated-config/plan.md | 1450 ++++------------- .../quickstart.md | 31 +- .../002-operator-generated-config/research.md | 52 +- .../review_summary.md | 249 ++- specs/002-operator-generated-config/spec.md | 85 +- specs/002-operator-generated-config/tasks.md | 1233 +++----------- 9 files changed, 843 insertions(+), 2434 deletions(-) diff --git a/specs/002-operator-generated-config/contracts/config-generation.yaml b/specs/002-operator-generated-config/contracts/config-generation.yaml index 19d59689f..119225ec6 100644 --- a/specs/002-operator-generated-config/contracts/config-generation.yaml +++ b/specs/002-operator-generated-config/contracts/config-generation.yaml @@ -22,7 +22,8 @@ provider_mapping: provider: provider_type # With "remote::" prefix (e.g., vllm โ†’ remote::vllm) endpoint: config.url apiKey: config.api_key # Via env var substitution: ${env.LLSD__API_KEY} - settings.*: config.* # Merged into provider config section + secretRefs.: config. # Each entry resolved to env var: ${env.LLSD__} + settings.*: config.* # Merged into provider config section (NO secret resolution) id: provider_id # Direct mapping # --- Env Var Naming Convention --- @@ -65,7 +66,22 @@ resource_mapping: merge_rules: providers: strategy: replace_by_api_type - description: "User providers fully replace base config providers for that API type" + description: > + When user specifies providers for an API type, ALL base config providers + for that API type are replaced. Base providers with unmatched IDs are NOT + preserved. If the user wants to keep a base provider, they must re-declare + it in their provider list. + examples: + - description: "Full replacement" + base: [{provider_id: base-inf, provider_type: "remote::vllm"}] + user: [{id: custom-inf, provider: ollama, endpoint: "http://ollama:11434"}] + result: [{provider_id: custom-inf, provider_type: "remote::ollama", config: {url: "http://ollama:11434"}}] + note: "base-inf is removed because user specified inference providers" + - description: "No user providers for API type" + base: [{provider_id: base-inf, provider_type: "remote::vllm"}] + user: [] # providers.inference not specified + result: [{provider_id: base-inf, provider_type: "remote::vllm"}] + note: "Base providers preserved when user does not specify that API type" storage: strategy: merge_by_subsection @@ -83,19 +99,31 @@ merge_rules: strategy: override description: "User value replaces base value" -# --- Settings SecretKeyRef Discovery --- -settings_secret_resolution: - depth: top_level_only - description: "secretKeyRef is recognized only at the top level of settings values" - recognized_pattern: "settings..secretKeyRef" - ignored_pattern: "settings...<...>.secretKeyRef" +# --- Secret Resolution --- +secret_resolution: + description: > + Secrets are resolved from two explicit typed fields only: apiKey and secretRefs. + The settings map is NEVER inspected for secret references. This eliminates + heuristic matching (any map with name+key fields) and provides a clear, + unambiguous boundary for secret resolution. + sources: + - field: "apiKey.secretKeyRef" + env_var_suffix: "API_KEY" + description: "Standard API key authentication" + - field: "secretRefs..secretKeyRef" + env_var_suffix: "" + description: "Named secret references for provider-specific connection fields" examples: - - path: "settings.host.secretKeyRef" - resolved: true - description: "Top-level value, resolved to env var" - - path: "settings.database.connection.secretKeyRef" - resolved: false - description: "Nested value, passed through as literal map" + - provider_id: "pgvector" + field: "secretRefs.host" + secret: {name: pg-creds, key: host} + env_var: "LLSD_PGVECTOR_HOST" + config_value: "${env.LLSD_PGVECTOR_HOST}" + - provider_id: "vllm-primary" + field: "apiKey" + secret: {name: vllm-creds, key: token} + env_var: "LLSD_VLLM_PRIMARY_API_KEY" + config_value: "${env.LLSD_VLLM_PRIMARY_API_KEY}" # --- ConfigMap Naming --- configmap: diff --git a/specs/002-operator-generated-config/contracts/crd-schema.yaml b/specs/002-operator-generated-config/contracts/crd-schema.yaml index 9de2738fc..687842789 100644 --- a/specs/002-operator-generated-config/contracts/crd-schema.yaml +++ b/specs/002-operator-generated-config/contracts/crd-schema.yaml @@ -16,25 +16,25 @@ spec: # --- Providers (optional, mutually exclusive with overrideConfig) --- providers: - inference: ProviderConfigOrList # Single object or list - safety: ProviderConfigOrList - vectorIo: ProviderConfigOrList - toolRuntime: ProviderConfigOrList - telemetry: ProviderConfigOrList + inference: []ProviderConfig # Always a list (single provider = one-element list) + safety: []ProviderConfig + vectorIo: []ProviderConfig + toolRuntime: []ProviderConfig + telemetry: []ProviderConfig # ProviderConfig schema: - # - id: string (required when list form; auto-generated for single form) + # - id: string (required when list has >1 element; auto-generated for single-element lists) # - provider: string (required, maps to provider_type with remote:: prefix) # - endpoint: string (optional, maps to config.url) # - apiKey: SecretKeyRef (optional, resolved to env var) - # - settings: map[string]any (optional, escape hatch merged into config) + # - secretRefs: map[string]SecretKeyRef (optional, named secret references resolved to env vars) + # - settings: map[string]any (optional, escape hatch merged into config, NO secret resolution) # --- Resources (optional, mutually exclusive with overrideConfig) --- resources: - models: # ModelConfigOrString[] - - string # Simple form: model name (uses first inference provider) - - name: string # Object form - provider: string # Provider ID reference (optional) + models: # []ModelConfig + - name: string # Required: model identifier + provider: string # Provider ID reference (optional, defaults to first inference provider) contextLength: int modelType: string quantization: string @@ -73,7 +73,9 @@ spec: configMapName: string configMapNamespace: string # Optional, defaults to CR namespace configMapKeys: [string] # Optional, defaults to ca-bundle.crt - expose: ExposeConfig # Polymorphic: bool or {hostname: string} + expose: + enabled: bool # Enable external access (default: false) + hostname: string # Custom hostname for Ingress/Route (optional) allowedFrom: namespaces: [string] labels: [string] # Namespace label keys for NetworkPolicy @@ -116,10 +118,23 @@ spec: configMapName: string # --- CEL Validation Rules --- +# Mutual exclusivity: # 1. !(has(self.providers) && has(self.overrideConfig)) # 2. !(has(self.resources) && has(self.overrideConfig)) # 3. !(has(self.storage) && has(self.overrideConfig)) # 4. !(has(self.disabled) && has(self.overrideConfig)) # 5. !(has(self.distribution.name) && has(self.distribution.image)) -# 6. When providers. is a list, each item must have id -# 7. All provider IDs must be unique across all provider types +# +# Provider ID validation (now feasible because providers are typed slices): +# 6. Per-type: self.providers.inference.size() <= 1 || self.providers.inference.all(p, has(p.id)) +# (same rule for safety, vectorIo, toolRuntime, telemetry) +# 7. All provider IDs unique across all provider types (union of all IDs has no duplicates) +# +# Disabled + provider conflict: +# 8. No provider type may appear in both providers and disabled +# e.g., !(has(self.providers.inference) && self.providers.inference.size() > 0 && self.disabled.exists(d, d == 'inference')) +# +# Conditional field requirements: +# 9. !has(self.networking.tls) || !self.networking.tls.enabled || has(self.networking.tls.secretName) +# 10. !has(self.storage.kv) || self.storage.kv.type != 'redis' || has(self.storage.kv.endpoint) +# 11. !has(self.storage.sql) || self.storage.sql.type != 'postgres' || has(self.storage.sql.connectionString) diff --git a/specs/002-operator-generated-config/data-model.md b/specs/002-operator-generated-config/data-model.md index f3ab879f0..3a25168e6 100644 --- a/specs/002-operator-generated-config/data-model.md +++ b/specs/002-operator-generated-config/data-model.md @@ -9,14 +9,14 @@ LlamaStackDistribution (CR) โ”œโ”€โ”€ Spec โ”‚ โ”œโ”€โ”€ DistributionSpec # Image source (name or direct image) -โ”‚ โ”œโ”€โ”€ ProvidersSpec # Provider configuration (polymorphic per API type) -โ”‚ โ”‚ โ”œโ”€โ”€ Inference # ProviderConfigOrList -โ”‚ โ”‚ โ”œโ”€โ”€ Safety # ProviderConfigOrList -โ”‚ โ”‚ โ”œโ”€โ”€ VectorIo # ProviderConfigOrList -โ”‚ โ”‚ โ”œโ”€โ”€ ToolRuntime # ProviderConfigOrList -โ”‚ โ”‚ โ””โ”€โ”€ Telemetry # ProviderConfigOrList +โ”‚ โ”œโ”€โ”€ ProvidersSpec # Provider configuration (typed slices per API type) +โ”‚ โ”‚ โ”œโ”€โ”€ Inference # []ProviderConfig +โ”‚ โ”‚ โ”œโ”€โ”€ Safety # []ProviderConfig +โ”‚ โ”‚ โ”œโ”€โ”€ VectorIo # []ProviderConfig +โ”‚ โ”‚ โ”œโ”€โ”€ ToolRuntime # []ProviderConfig +โ”‚ โ”‚ โ””โ”€โ”€ Telemetry # []ProviderConfig โ”‚ โ”œโ”€โ”€ ResourcesSpec # Registered resources -โ”‚ โ”‚ โ”œโ”€โ”€ Models # ModelConfigOrString[] +โ”‚ โ”‚ โ”œโ”€โ”€ Models # []ModelConfig โ”‚ โ”‚ โ”œโ”€โ”€ Tools # string[] โ”‚ โ”‚ โ””โ”€โ”€ Shields # string[] โ”‚ โ”œโ”€โ”€ StorageSpec # State storage backends @@ -26,7 +26,7 @@ LlamaStackDistribution (CR) โ”‚ โ”œโ”€โ”€ NetworkingSpec # Network configuration โ”‚ โ”‚ โ”œโ”€โ”€ Port # int32 โ”‚ โ”‚ โ”œโ”€โ”€ TLS # TLSSpec -โ”‚ โ”‚ โ”œโ”€โ”€ Expose # ExposeConfig (polymorphic) +โ”‚ โ”‚ โ”œโ”€โ”€ Expose # ExposeConfig (enabled + hostname) โ”‚ โ”‚ โ””โ”€โ”€ AllowedFrom # AllowedFromSpec โ”‚ โ”œโ”€โ”€ WorkloadSpec # Deployment settings โ”‚ โ”‚ โ”œโ”€โ”€ Replicas # *int32 @@ -80,32 +80,35 @@ LlamaStackDistribution (CR) | Field | Type | Required | Default | Validation | Description | |-------|------|----------|---------|------------|-------------| -| `id` | string | Conditional | Auto-generated from `provider` (FR-035) | Required when multiple providers per API type (FR-034) | Unique provider identifier | +| `id` | string | Conditional | Auto-generated from `provider` (FR-035) | Required when list has >1 element (FR-034, CEL) | Unique provider identifier | | `provider` | string | Yes | - | Required | Provider type (e.g., `vllm`, `llama-guard`, `pgvector`) | | `endpoint` | string | No | - | URL format | Provider endpoint URL | | `apiKey` | *SecretKeyRef | No | - | - | Secret reference for API authentication | -| `settings` | map[string]interface{} | No | - | Unstructured (escape hatch) | Provider-specific settings merged into config | +| `secretRefs` | map[string]SecretKeyRef | No | - | - | Named secret references for provider-specific connection fields | +| `settings` | map[string]interface{} | No | - | Unstructured (escape hatch) | Provider-specific settings merged into config (NO secret resolution) | **Mapping to config.yaml**: - `provider` maps to `provider_type` with `remote::` prefix (FR-030) - `endpoint` maps to `config.url` (FR-031) - `apiKey` maps to `config.api_key` via env var `${env.LLSD__API_KEY}` (FR-032) -- `settings.*` merged into `config.*` (FR-033) +- `secretRefs.` maps to `config.` via env var `${env.LLSD__}` (FR-032) +- `settings.*` merged into `config.*` (FR-033), passed through without secret resolution --- -### ProviderConfigOrList +### Provider Lists -**Purpose**: Polymorphic wrapper allowing single provider object or list of providers. +**Purpose**: Each provider API type field is a typed `[]ProviderConfig` slice. A single provider is expressed as a one-element list. This provides kubebuilder validation, IDE autocompletion, and CEL inspection support. -| Form | Example | ID Requirement | -|------|---------|----------------| -| Single object | `inference: {provider: vllm, endpoint: "..."}` | Optional (auto-generated from `provider`) | -| List | `inference: [{id: primary, provider: vllm, ...}, {id: fallback, ...}]` | Required on each item | +| Scenario | Example | ID Requirement | +|----------|---------|----------------| +| Single provider | `inference: [{provider: vllm, endpoint: "..."}]` | Optional (auto-generated from `provider`) | +| Multiple providers | `inference: [{id: primary, provider: vllm, ...}, {id: fallback, ...}]` | Required on each item | -**Validation rules**: -- When list form: each item MUST have explicit `id` (FR-034, CEL) -- All provider IDs MUST be unique across all API types (FR-072, CEL) +**Validation rules (CEL)**: +- When list has >1 element: each item MUST have explicit `id` (FR-034) +- All provider IDs MUST be unique across all API types (FR-072) +- No provider type may appear in both `providers` and `disabled` (FR-079d) --- @@ -130,25 +133,25 @@ LlamaStackDistribution (CR) | Field | Type | Required | Default | Validation | Description | |-------|------|----------|---------|------------|-------------| -| `models` | []ModelConfigOrString | No | - | - | Models to register | +| `models` | []ModelConfig | No | - | - | Models to register | | `tools` | []string | No | - | - | Tool groups to register | | `shields` | []string | No | - | - | Safety shields to register | --- -### ModelConfig (full form of ModelConfigOrString) +### ModelConfig -**Purpose**: Detailed model registration with provider assignment. +**Purpose**: Model registration with optional provider assignment and metadata. | Field | Type | Required | Default | Validation | Description | |-------|------|----------|---------|------------|-------------| -| `name` | string | Yes | - | - | Model identifier (e.g., `llama3.2-8b`) | +| `name` | string | Yes | - | kubebuilder Required | Model identifier (e.g., `llama3.2-8b`) | | `provider` | string | No | First inference provider | Must reference valid provider ID (webhook) | Provider ID for this model | | `contextLength` | int | No | - | - | Model context window size | | `modelType` | string | No | - | - | Model type classification | | `quantization` | string | No | - | - | Quantization method | -**Polymorphic**: Can be specified as a simple string (just the model name) or a full object. Simple string form uses the first inference provider. +**Usage**: Always specified as a typed struct. For simple model references, only `name` is required (e.g., `{name: "llama3.2-8b"}`). The first inference provider is used when `provider` is omitted. --- @@ -197,23 +200,20 @@ LlamaStackDistribution (CR) --- -### ExposeConfig (polymorphic) +### ExposeConfig **Purpose**: Controls external service exposure via Ingress/Route. -| Form | Value | Behavior | -|------|-------|----------| -| Boolean true | `expose: true` | Create Ingress/Route with auto-generated hostname | -| Empty object | `expose: {}` | Treated as `expose: true` | -| Object with hostname | `expose: {hostname: "llama.example.com"}` | Create Ingress/Route with specified hostname | -| Not specified / false | `expose: false` or omitted | No external access | - -**Go representation**: +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | *bool | No | false | Enable external access via Ingress/Route | +| `hostname` | string | No | auto-generated | Custom hostname for Ingress/Route | -| Field | Type | Description | -|-------|------|-------------| -| `enabled` | *bool | Explicit enable/disable | -| `hostname` | string | Custom hostname for Ingress/Route | +**Behavior**: +- `expose: {enabled: true}`: Create Ingress/Route with auto-generated hostname +- `expose: {enabled: true, hostname: "llama.example.com"}`: Create with specified hostname +- `expose: {}`: Treated as enabled with defaults (presence implies intent) +- Not specified: No external access --- @@ -411,7 +411,7 @@ ExternalProviders (spec 001) โ”€โ”€โ–บ Merged into config โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ | `PodOverrides` | `WorkloadOverrides` | Rename + expand | | `PodDisruptionBudgetSpec` | `WorkloadSpec.PodDisruptionBudget` | Direct move | | `TopologySpreadConstraints` | `WorkloadSpec.TopologySpreadConstraints` | Direct move | -| `NetworkSpec.ExposeRoute` | `NetworkingSpec.Expose` | Bool to polymorphic | +| `NetworkSpec.ExposeRoute` | `NetworkingSpec.Expose` | Bool to typed struct (enabled + hostname) | | `NetworkSpec.AllowedFrom` | `NetworkingSpec.AllowedFrom` | Direct move | | *(new)* | `ProvidersSpec` | New in v1alpha2 | | *(new)* | `ResourcesSpec` | New in v1alpha2 | diff --git a/specs/002-operator-generated-config/plan.md b/specs/002-operator-generated-config/plan.md index e80fae684..0eec5360c 100644 --- a/specs/002-operator-generated-config/plan.md +++ b/specs/002-operator-generated-config/plan.md @@ -1,42 +1,47 @@ # Implementation Plan: Operator-Generated Server Configuration (v1alpha2) -**Branch**: `002-operator-generated-config` | **Date**: 2026-02-02 | **Spec**: [spec.md](spec.md) -**Status**: Ready for Implementation +**Branch**: `002-reimpl` | **Date**: 2026-03-10 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/002-operator-generated-config/spec.md` ## Summary -Introduce a v1alpha2 API version for the LlamaStackDistribution CRD that enables the operator to generate server configuration (config.yaml) from a high-level, abstracted CR specification. Users configure providers, resources, and storage with minimal YAML while the operator handles config generation, secret resolution, and atomic Deployment updates. +Enable the operator to generate `config.yaml` from a high-level v1alpha2 CRD spec, replacing the requirement for users to provide a complete ConfigMap. The implementation uses typed `[]ProviderConfig` slices (no polymorphic JSON), a `ConfigResolver` interface for base config extraction, immutable content-hashed ConfigMaps, and atomic Deployment updates. Phase 1 embeds default configs; Phase 2 (future) adds OCI label extraction. ## Technical Context -**Language/Version**: Go 1.25 (go.mod) +**Language/Version**: Go 1.25 (from go.mod) **Primary Dependencies**: controller-runtime v0.22.4, kubebuilder, kustomize/api v0.21.0, client-go v0.34.3, go-containerregistry v0.20.7 -**Storage**: Kubernetes ConfigMaps (generated), Secrets (referenced via secretKeyRef) +**Storage**: Kubernetes ConfigMaps (generated, immutable with content-hash), Secrets (referenced via secretKeyRef) **Testing**: Go test, envtest (controller-runtime), testify v1.11.1 -**Target Platform**: Kubernetes 1.30+ +**Target Platform**: Kubernetes 1.30+ (controller-runtime v0.22.x baseline) **Project Type**: Kubernetes operator (single binary) **Performance Goals**: Config generation < 5 seconds (NFR-002) -**Constraints**: Namespace-scoped RBAC (constitution ยง1.1), air-gapped registry support, deterministic output (NFR-001) -**Scale/Scope**: Single CRD with 2 API versions, ~8 new Go packages +**Constraints**: Namespace-scoped RBAC, air-gapped registry support, deterministic output, FIPS compliance +**Scale/Scope**: Single CRD (LlamaStackDistribution), 5 provider API types, ~20 new source files ## Constitution Check -*GATE: PASS (1 documented deviation, 0 unresolved violations)* - -| # | Principle | Status | Notes | -|---|-----------|--------|-------| -| ยง1.1 Namespace-Scoped | DEVIATION | ValidatingWebhookConfiguration is cluster-scoped (standard operator pattern). Documented in spec.md Security Considerations. | -| ยง1.2 Idempotent Reconciliation | PASS | Deterministic config generation (NFR-001). Hash-based change detection. | -| ยง1.3 Owner References | PASS | FR-025 requires owner refs on generated ConfigMaps. | -| ยง2.1 Kubebuilder Validation | PASS | CEL (FR-070-072), webhook (FR-076-078), kubebuilder tags. | -| ยง2.2 Optional Fields | PASS | Pointer types for optional structs throughout. | -| ยง2.3 Defaults | PASS | Constants for DefaultServerPort, storage type defaults. | -| ยง2.4 Status Subresource | PASS | New conditions: ConfigGenerated, DeploymentUpdated, Available, SecretsResolved. | -| ยง3.2 Conditions | PASS | Standard metav1.Condition with defined constants for types, reasons, messages. | -| ยง4.1 Error Wrapping | PASS | All errors wrapped with %w and context. | -| ยง6.1 Table-Driven Tests | PASS | Test plan follows constitution patterns. | -| ยง6.4 Builder Pattern | PASS | Existing test builders extended for v1alpha2. | -| ยง13.2 AI Attribution | PASS | Assisted-by format (no Co-Authored-By). | +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Rule | Section | Status | Notes | +|------|---------|--------|-------| +| Namespace-scoped resources | ยง1.1 | PASS | All resources namespace-scoped. Webhook is cluster-scoped (accepted deviation, documented in spec Security Considerations) | +| Idempotent reconciliation | ยง1.2 | PASS | Config generation is deterministic (NFR-001), ConfigMap creation is idempotent via content hash | +| Owner references | ยง1.3 | PASS | Generated ConfigMaps use owner references (FR-025) | +| Kubebuilder validation | ยง2.1 | PASS | All provider/resource fields are typed slices with kubebuilder markers. CEL rules for cross-field validation (FR-070-072, FR-079) | +| Optional field pointers | ยง2.2 | PASS | Optional structs use pointers (StorageSpec, NetworkingSpec, WorkloadSpec) | +| Status phase + conditions | ยง2.4/ยง3 | PASS | Phase enum + 4 condition types (ConfigGenerated, DeploymentUpdated, Available, SecretsResolved) | +| Error wrapping | ยง4.1 | PASS | All errors wrapped with context using `%w` | +| Structured logging | ยง5.1 | PASS | Logger from context with structured fields | +| Table-driven tests | ยง6.1 | PASS | All test files use table-driven pattern | +| Builder pattern for tests | ยง6.4 | PASS | Extend existing `DistributionBuilder` for v1alpha2 | +| FIPS compliance | ยง14 | PASS | SHA-256 for content hashing (FR-024), no non-FIPS algorithms | + +**Deviation requiring justification:** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| ValidatingWebhookConfiguration is cluster-scoped | Required by Kubernetes API for admission webhooks | No alternative exists; this is the standard pattern for all operators with webhooks. Webhook is installed by OLM/kustomize, not by operator at runtime. | ## Project Structure @@ -44,1166 +49,345 @@ Introduce a v1alpha2 API version for the LlamaStackDistribution CRD that enables ```text specs/002-operator-generated-config/ -โ”œโ”€โ”€ spec.md # Feature specification โ”œโ”€โ”€ plan.md # This file -โ”œโ”€โ”€ research.md # Phase 0 research decisions +โ”œโ”€โ”€ spec.md # Feature specification (updated with clarifications) +โ”œโ”€โ”€ research.md # Technical decisions and rationale โ”œโ”€โ”€ data-model.md # Entity definitions and relationships -โ”œโ”€โ”€ quickstart.md # Usage examples -โ”œโ”€โ”€ contracts/ # Interface contracts -โ”‚ โ”œโ”€โ”€ crd-schema.yaml -โ”‚ โ”œโ”€โ”€ config-generation.yaml -โ”‚ โ””โ”€โ”€ status-conditions.yaml -โ”œโ”€โ”€ tasks.md # Implementation tasks +โ”œโ”€โ”€ quickstart.md # User-facing configuration examples โ”œโ”€โ”€ review_summary.md # Executive brief -โ””โ”€โ”€ alternatives/ # Alternative approaches evaluated +โ”œโ”€โ”€ contracts/ +โ”‚ โ”œโ”€โ”€ config-generation.yaml # Config generator interface contract +โ”‚ โ”œโ”€โ”€ crd-schema.yaml # CRD schema reference +โ”‚ โ””โ”€โ”€ status-conditions.yaml # Status conditions contract +โ”œโ”€โ”€ alternatives/ +โ”‚ โ””โ”€โ”€ init-container-extraction.md # Deferred alternative approach +โ””โ”€โ”€ tasks.md # Task breakdown (generated by /speckit.tasks) ``` ### Source Code (repository root) ```text api/ -โ”œโ”€โ”€ v1alpha1/ # Existing types + conversion spoke -โ”‚ โ”œโ”€โ”€ llamastackdistribution_types.go -โ”‚ โ””โ”€โ”€ llamastackdistribution_conversion.go # New: v1alpha1 spoke -โ””โ”€โ”€ v1alpha2/ # New API version - โ”œโ”€โ”€ groupversion_info.go - โ”œโ”€โ”€ llamastackdistribution_types.go - โ”œโ”€โ”€ llamastackdistribution_webhook.go # Validating webhook - โ”œโ”€โ”€ llamastackdistribution_conversion.go # Hub (no-op) - โ””โ”€โ”€ zz_generated.deepcopy.go # Generated - -pkg/config/ # New: config generation engine -โ”œโ”€โ”€ config.go # Main orchestration -โ”œโ”€โ”€ generator.go # YAML generation -โ”œโ”€โ”€ resolver.go # Base config resolution -โ”œโ”€โ”€ provider.go # Provider expansion -โ”œโ”€โ”€ resource.go # Resource expansion -โ”œโ”€โ”€ storage.go # Storage configuration -โ”œโ”€โ”€ secret_resolver.go # Secret reference resolution -โ”œโ”€โ”€ version.go # Config schema version handling -โ”œโ”€โ”€ types.go # Internal config types -โ””โ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction - -configs/ # New: embedded default configs -โ”œโ”€โ”€ starter/config.yaml -โ”œโ”€โ”€ remote-vllm/config.yaml -โ”œโ”€โ”€ meta-reference-gpu/config.yaml -โ””โ”€โ”€ postgres-demo/config.yaml - -controllers/ # Extended for v1alpha2 -โ”œโ”€โ”€ llamastackdistribution_controller.go # Updated reconciliation -โ””โ”€โ”€ status.go # New conditions - -config/webhook/ # New: webhook kustomize config -โ””โ”€โ”€ manifests.yaml - -tests/e2e/ # Extended -โ””โ”€โ”€ config_generation_test.go # New: v1alpha2 e2e tests -``` - -## Implementation Phases - -The implementation is divided into 5 phases, designed to allow incremental delivery and testing: - -``` -Phase 1: CRD Schema (v1alpha2) โ”€โ”€โ”€โ”€โ”€โ–บ Foundation -Phase 2: Config Generation Engine โ”€โ”€โ”€โ”€โ”€โ–บ Core Logic -Phase 3: Controller Integration โ”€โ”€โ”€โ”€โ”€โ–บ Reconciliation -Phase 4: Conversion Webhook โ”€โ”€โ”€โ”€โ”€โ–บ Backward Compat -Phase 5: Testing & Documentation โ”€โ”€โ”€โ”€โ”€โ–บ Quality Gates -``` - ---- - -## Phase 1: CRD Schema (v1alpha2) - -**Goal**: Define the new v1alpha2 API types with all new fields. - -**Requirements Covered**: FR-001 to FR-014, FR-070 to FR-072 - -### Tasks - -#### 1.1 Create v1alpha2 API Directory Structure - -**Files to create**: -- `api/v1alpha2/groupversion_info.go` -- `api/v1alpha2/llamastackdistribution_types.go` -- `api/v1alpha2/zz_generated.deepcopy.go` (generated) - -**Approach**: -1. Copy v1alpha1 as starting point -2. Restructure according to spec schema -3. Add new types for providers, resources, storage, networking, workload - -#### 1.2 Define Provider Types +โ”œโ”€โ”€ v1alpha1/ +โ”‚ โ”œโ”€โ”€ llamastackdistribution_types.go # Existing types (unchanged) +โ”‚ โ”œโ”€โ”€ llamastackdistribution_conversion.go # NEW: spoke conversion (v1alpha1 โ†” v1alpha2) +โ”‚ โ””โ”€โ”€ llamastackdistribution_conversion_test.go +โ”œโ”€โ”€ v1alpha2/ +โ”‚ โ”œโ”€โ”€ doc.go # NEW: package doc +โ”‚ โ”œโ”€โ”€ groupversion_info.go # NEW: scheme registration +โ”‚ โ”œโ”€โ”€ llamastackdistribution_types.go # NEW: v1alpha2 types (hub) +โ”‚ โ”œโ”€โ”€ llamastackdistribution_conversion.go # NEW: hub conversion marker +โ”‚ โ”œโ”€โ”€ llamastackdistribution_webhook.go # NEW: validating webhook +โ”‚ โ”œโ”€โ”€ llamastackdistribution_webhook_test.go +โ”‚ โ””โ”€โ”€ zz_generated.deepcopy.go # Generated + +pkg/ +โ”œโ”€โ”€ config/ # NEW: config generation package +โ”‚ โ”œโ”€โ”€ generator.go # Orchestrator: GenerateConfig() +โ”‚ โ”œโ”€โ”€ generator_test.go +โ”‚ โ”œโ”€โ”€ resolver.go # ConfigResolver interface + EmbeddedConfigResolver +โ”‚ โ”œโ”€โ”€ resolver_test.go +โ”‚ โ”œโ”€โ”€ provider.go # Provider expansion (CRD โ†’ config.yaml) +โ”‚ โ”œโ”€โ”€ provider_test.go +โ”‚ โ”œโ”€โ”€ resource.go # Resource registration expansion +โ”‚ โ”œโ”€โ”€ resource_test.go +โ”‚ โ”œโ”€โ”€ storage.go # Storage config generation +โ”‚ โ”œโ”€โ”€ storage_test.go +โ”‚ โ”œโ”€โ”€ secret.go # Secret reference collection and env var generation +โ”‚ โ”œโ”€โ”€ secret_test.go +โ”‚ โ”œโ”€โ”€ merge.go # Base config merge logic +โ”‚ โ”œโ”€โ”€ merge_test.go +โ”‚ โ””โ”€โ”€ version.go # Config version detection +โ”œโ”€โ”€ config/testdata/ # Embedded test configs +โ”‚ โ”œโ”€โ”€ starter/config.yaml +โ”‚ โ””โ”€โ”€ remote-vllm/config.yaml +โ”œโ”€โ”€ deploy/ # Existing: distribution registry +โ”‚ โ””โ”€โ”€ distributions.json # Existing: name โ†’ image mapping +โ””โ”€โ”€ deploy/configs/ # NEW: embedded default configs + โ”œโ”€โ”€ starter/config.yaml + โ””โ”€โ”€ remote-vllm/config.yaml + +controllers/ +โ”œโ”€โ”€ llamastackdistribution_controller.go # Modified: add v1alpha2 reconciliation path +โ”œโ”€โ”€ llamastackdistribution_controller_test.go # Modified: add v1alpha2 tests +โ”œโ”€โ”€ configmap_reconciler.go # NEW: ConfigMap creation, hash comparison, cleanup +โ”œโ”€โ”€ configmap_reconciler_test.go +โ”œโ”€โ”€ v1alpha2_helpers.go # NEW: v1alpha2-specific controller helpers +โ””โ”€โ”€ v1alpha2_helpers_test.go + +config/ +โ”œโ”€โ”€ crd/bases/ # Modified: regenerated CRD with v1alpha2 +โ”œโ”€โ”€ crd/patches/ +โ”‚ โ””โ”€โ”€ webhook_in_llamastackdistributions.yaml # NEW: webhook CRD patch +โ”œโ”€โ”€ certmanager/ # NEW: cert-manager for webhook TLS +โ”‚ โ”œโ”€โ”€ certificate.yaml +โ”‚ โ”œโ”€โ”€ kustomization.yaml +โ”‚ โ””โ”€โ”€ kustomizeconfig.yaml +โ”œโ”€โ”€ default/ +โ”‚ โ”œโ”€โ”€ kustomization.yaml # Modified: enable webhook + certmanager +โ”‚ โ””โ”€โ”€ manager_webhook_patch.yaml # NEW: webhook volume mount +โ”œโ”€โ”€ openshift/ # NEW: OpenShift-specific webhook config +โ”‚ โ”œโ”€โ”€ kustomization.yaml +โ”‚ โ”œโ”€โ”€ crd_ca_patch.yaml +โ”‚ โ”œโ”€โ”€ manager_webhook_patch.yaml +โ”‚ โ””โ”€โ”€ webhook_ca_patch.yaml +โ””โ”€โ”€ samples/ + โ”œโ”€โ”€ v1alpha1/ # Moved: existing samples + โ””โ”€โ”€ v1alpha2/ # NEW: v1alpha2 samples + +tests/e2e/ # Modified: add v1alpha2 e2e tests +``` + +**Structure Decision**: Follows existing kubebuilder operator layout. New `pkg/config/` package isolates config generation logic from the controller, making it independently testable. The `api/v1alpha2/` package follows standard kubebuilder multi-version conventions. + +## Phase 0: Research + +All technical decisions have been researched and documented in [research.md](research.md). Key decisions: + +| Area | Decision | Risk | +|------|----------|------| +| Provider/resource types | Typed `[]ProviderConfig` slices (no polymorphism) | Low | +| Base config source | Embedded `go:embed` (Phase 1) + ConfigResolver interface | Low | +| Config merging | Deep merge with full API-type replacement semantics | Low | +| Env var naming | `LLSD__` | Low | +| Conversion | v1alpha2 hub, v1alpha1 spoke with annotation preservation | Low | +| Validation layers | CEL + webhook + controller | Low | +| ConfigMap pattern | Immutable with content-hash suffix, retain last 2 | Low | +| Deployment updates | Single atomic `client.Update()` | Low | +| Secret references | Explicit `apiKey` + `secretRefs` fields (no heuristic matching) | Low | + +## Phase 1: Design + +### 1.1 CRD Schema (v1alpha2 Types) + +**Files**: `api/v1alpha2/llamastackdistribution_types.go` + +Define the complete v1alpha2 type hierarchy: -**New types**: ```go -// ProviderSpec supports polymorphic single/list form via json.RawMessage +// ProvidersSpec uses typed slices for all provider fields. type ProvidersSpec struct { - Inference *ProviderConfigOrList `json:"inference,omitempty"` - Safety *ProviderConfigOrList `json:"safety,omitempty"` - VectorIo *ProviderConfigOrList `json:"vectorIo,omitempty"` - ToolRuntime *ProviderConfigOrList `json:"toolRuntime,omitempty"` - Telemetry *ProviderConfigOrList `json:"telemetry,omitempty"` + Inference []ProviderConfig `json:"inference,omitempty"` + Safety []ProviderConfig `json:"safety,omitempty"` + VectorIo []ProviderConfig `json:"vectorIo,omitempty"` + ToolRuntime []ProviderConfig `json:"toolRuntime,omitempty"` + Telemetry []ProviderConfig `json:"telemetry,omitempty"` } type ProviderConfig struct { - ID string `json:"id,omitempty"` - Provider string `json:"provider"` - Endpoint string `json:"endpoint,omitempty"` - ApiKey *SecretKeyRef `json:"apiKey,omitempty"` - Settings map[string]interface{} `json:"settings,omitempty"` + // +optional + ID string `json:"id,omitempty"` + // +kubebuilder:validation:Required + Provider string `json:"provider"` + // +optional + Endpoint string `json:"endpoint,omitempty"` + // +optional + APIKey *SecretKeyRef `json:"apiKey,omitempty"` + // +optional + SecretRefs map[string]SecretKeyRef `json:"secretRefs,omitempty"` + // +optional + Settings apiextensionsv1.JSON `json:"settings,omitempty"` } -``` - -**Polymorphic handling**: Use `json.RawMessage` for `ProviderConfigOrList`, parse at runtime. - -#### 1.3 Define Resource Types -**New types**: -```go type ResourcesSpec struct { - Models []ModelConfigOrString `json:"models,omitempty"` - Tools []string `json:"tools,omitempty"` - Shields []string `json:"shields,omitempty"` + Models []ModelConfig `json:"models,omitempty"` + Tools []string `json:"tools,omitempty"` + Shields []string `json:"shields,omitempty"` } type ModelConfig struct { - Name string `json:"name"` - Provider string `json:"provider,omitempty"` - ContextLength int `json:"contextLength,omitempty"` - ModelType string `json:"modelType,omitempty"` - Quantization string `json:"quantization,omitempty"` -} -``` - -#### 1.4 Define Storage Types - -**New types**: -```go -type StorageSpec struct { - KV *KVStorageSpec `json:"kv,omitempty"` - SQL *SQLStorageSpec `json:"sql,omitempty"` -} - -type KVStorageSpec struct { - Type string `json:"type,omitempty"` // sqlite, redis - Endpoint string `json:"endpoint,omitempty"` - Password *SecretKeyRef `json:"password,omitempty"` -} - -type SQLStorageSpec struct { - Type string `json:"type,omitempty"` // sqlite, postgres - ConnectionString *SecretKeyRef `json:"connectionString,omitempty"` -} -``` - -#### 1.5 Define Networking Types - -**New types**: -```go -type NetworkingSpec struct { - Port int32 `json:"port,omitempty"` - TLS *TLSSpec `json:"tls,omitempty"` - Expose *ExposeConfig `json:"expose,omitempty"` // Polymorphic - AllowedFrom *AllowedFromSpec `json:"allowedFrom,omitempty"` -} - -type ExposeConfig struct { - Enabled *bool `json:"enabled,omitempty"` - Hostname string `json:"hostname,omitempty"` -} -``` - -#### 1.6 Define Workload Types - -**New types**: -```go -type WorkloadSpec struct { - Replicas *int32 `json:"replicas,omitempty"` - Workers *int32 `json:"workers,omitempty"` - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` - Autoscaling *AutoscalingSpec `json:"autoscaling,omitempty"` - Storage *PVCStorageSpec `json:"storage,omitempty"` - PodDisruptionBudget *PodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` - TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` - Overrides *WorkloadOverrides `json:"overrides,omitempty"` -} - -type WorkloadOverrides struct { - ServiceAccountName string `json:"serviceAccountName,omitempty"` - Env []corev1.EnvVar `json:"env,omitempty"` - Command []string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - Volumes []corev1.Volume `json:"volumes,omitempty"` - VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` -} -``` - -#### 1.7 Add CEL Validation Rules - -**Validation rules**: -```go -// +kubebuilder:validation:XValidation:rule="!(has(self.providers) && has(self.overrideConfig))",message="providers and overrideConfig are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.resources) && has(self.overrideConfig))",message="resources and overrideConfig are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.storage) && has(self.overrideConfig))",message="storage and overrideConfig are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.disabled) && has(self.overrideConfig))",message="disabled and overrideConfig are mutually exclusive" -``` - -#### 1.8 Generate CRD Manifests - -**Commands**: -```bash -make generate -make manifests -``` - -**Verification**: -- CRD YAML generated in `config/crd/bases/` -- OpenAPI schema includes all new fields -- CEL validation rules appear in CRD - -### Deliverables - -- [ ] `api/v1alpha2/` package with all types -- [ ] Generated CRD manifests -- [ ] CEL validation for mutual exclusivity -- [ ] Unit tests for type marshaling/unmarshaling - ---- - -## Phase 2: Config Generation Engine - -**Goal**: Implement the core config generation logic. - -**Requirements Covered**: FR-020 to FR-029, FR-030 to FR-035, FR-040 to FR-044, FR-050 to FR-053, NFR-001, NFR-005, NFR-006 - -### Tasks - -#### 2.1 Create Config Package Structure - -**Directory**: `pkg/config/` - -**Files**: -``` -pkg/config/ -โ”œโ”€โ”€ config.go # Main orchestration -โ”œโ”€โ”€ generator.go # YAML generation -โ”œโ”€โ”€ resolver.go # Base config resolution (embedded + OCI) -โ”œโ”€โ”€ oci_extractor.go # Phase 2: OCI label extraction -โ”œโ”€โ”€ provider.go # Provider expansion -โ”œโ”€โ”€ resource.go # Resource expansion -โ”œโ”€โ”€ storage.go # Storage configuration -โ”œโ”€โ”€ secret_resolver.go # Secret reference resolution -โ”œโ”€โ”€ version.go # Config schema version handling -โ””โ”€โ”€ types.go # Internal config types -``` - -#### 2.2 Implement Base Config Resolution (Phased) - -**Files**: -- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic -- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) -- `pkg/config/oci_extractor.go` - Phase 2: OCI label extraction - -**Approach**: Base config resolution follows a phased strategy. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`, requiring no changes to distribution image builds. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override when distribution images support it. - -> **Alternative**: An init container approach is documented in `alternatives/init-container-extraction.md` for cases where neither embedded configs nor OCI labels are available. - -**Resolution Priority**: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ 1. (Phase 2) Check OCI labels on resolved image โ”‚ -โ”‚ โ””โ”€โ”€ If present: extract config from labels โ”‚ -โ”‚ (takes precedence over embedded) โ”‚ -โ”‚ โ”‚ -โ”‚ 2. (Phase 1) Check embedded configs for distribution.name โ”‚ -โ”‚ โ””โ”€โ”€ If found: use go:embed config for that distribution โ”‚ -โ”‚ โ”‚ -โ”‚ 3. No config available: โ”‚ -โ”‚ โ”œโ”€โ”€ distribution.name โ†’ should not happen (build error) โ”‚ -โ”‚ โ””โ”€โ”€ distribution.image โ†’ require overrideConfig โ”‚ -โ”‚ (set ConfigGenerated=False, reason BaseConfigReq.) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -##### Phase 1: Embedded Default Configs (MVP) - -The operator binary embeds default configs for all named distributions via `go:embed`: - -```go -// pkg/config/resolver.go - -import "embed" - -//go:embed configs -var embeddedConfigs embed.FS - -type BaseConfigResolver struct { - distributionImages map[string]string // from distributions.json - imageOverrides map[string]string // from operator ConfigMap - ociExtractor *ImageConfigExtractor // nil in Phase 1 -} - -func NewBaseConfigResolver(distImages, overrides map[string]string) *BaseConfigResolver { - return &BaseConfigResolver{ - distributionImages: distImages, - imageOverrides: overrides, - } -} - -func (r *BaseConfigResolver) Resolve(ctx context.Context, dist DistributionSpec) (*BaseConfig, string, error) { - // Resolve distribution to concrete image reference - image, err := r.resolveImage(dist) - if err != nil { - return nil, "", err - } - - // Phase 2: Check OCI labels first (when ociExtractor is configured) - if r.ociExtractor != nil { - config, err := r.ociExtractor.Extract(ctx, image) - if err == nil { - return config, image, nil - } - // Fall through to embedded if OCI extraction fails - log.FromContext(ctx).V(1).Info("OCI config extraction failed, falling back to embedded", - "image", image, "error", err) - } - - // Phase 1: Use embedded config for named distributions - if dist.Name != "" { - config, err := r.loadEmbeddedConfig(dist.Name) - if err != nil { - return nil, "", fmt.Errorf("failed to load embedded config for distribution %q: %w", dist.Name, err) - } - return config, image, nil - } - - // distribution.image without OCI labels or embedded config - return nil, "", fmt.Errorf("direct image references require either overrideConfig.configMapName or OCI config labels on the image") -} - -func (r *BaseConfigResolver) loadEmbeddedConfig(name string) (*BaseConfig, error) { - data, err := embeddedConfigs.ReadFile(fmt.Sprintf("configs/%s/config.yaml", name)) - if err != nil { - return nil, fmt.Errorf("no embedded config for distribution %q: %w", name, err) - } - - var config BaseConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("invalid embedded config for distribution %q: %w", name, err) - } - - return &config, nil -} - -func (r *BaseConfigResolver) resolveImage(dist DistributionSpec) (string, error) { - if dist.Image != "" { - return dist.Image, nil - } - - // Check image-overrides first (downstream builds) - if override, ok := r.imageOverrides[dist.Name]; ok { - return override, nil - } - - // Look up in distributions.json - if image, ok := r.distributionImages[dist.Name]; ok { - return image, nil - } - - return "", fmt.Errorf("unknown distribution name %q: not found in distributions.json", dist.Name) -} -``` - -**Embedded config directory** (created at build time): -``` -configs/ -โ”œโ”€โ”€ starter/config.yaml -โ”œโ”€โ”€ remote-vllm/config.yaml -โ”œโ”€โ”€ meta-reference-gpu/config.yaml -โ””โ”€โ”€ postgres-demo/config.yaml -``` - -**Air-gapped support**: Embedded configs work regardless of registry access. The `image-overrides` mechanism allows downstream builds (e.g., RHOAI) to remap distribution names to internal registry images without rebuilding the operator. - -##### Phase 2: OCI Label Extraction (Enhancement) - -**File**: `pkg/config/oci_extractor.go` - -When distribution images include OCI config labels, the extracted config takes precedence over embedded defaults. This enables `distribution.image` usage without `overrideConfig`. - -**OCI Label Convention**: - -| Label | Purpose | When Used | -|-------|---------|-----------| -| `io.llamastack.config.base64` | Base64-encoded config.yaml | Small configs (< 50KB) | -| `io.llamastack.config.layer` | Layer digest containing config | Large configs | -| `io.llamastack.config.path` | Path within the layer | Used with layer reference | -| `io.llamastack.config.version` | Config schema version | Always | - -**Registry Authentication**: - -Uses `k8schain` from `go-containerregistry` to authenticate the same way kubelet does: - -```go -import ( - "github.com/google/go-containerregistry/pkg/authn/k8schain" - "github.com/google/go-containerregistry/pkg/crane" -) - -// k8schain checks (in order): -// 1. ServiceAccount imagePullSecrets -// 2. Namespace default ServiceAccount -// 3. Node credentials (GCR, ECR, ACR) -// 4. Anonymous access -``` - -**Implementation**: - -```go -// pkg/config/oci_extractor.go - -type ConfigLocation struct { - Base64 string // Inline base64 encoded config - LayerDigest string // Layer digest containing config file - Path string // Path within the layer - Version string // Config schema version -} - -type ImageConfigExtractor struct { - k8sClient client.Client - namespace string - serviceAccount string - cache *sync.Map // image digest -> BaseConfig -} - -func NewImageConfigExtractor(client client.Client, namespace, sa string) *ImageConfigExtractor { - return &ImageConfigExtractor{ - k8sClient: client, - namespace: namespace, - serviceAccount: sa, - cache: &sync.Map{}, - } -} - -func (e *ImageConfigExtractor) Extract(ctx context.Context, imageRef string) (*BaseConfig, error) { - // Build keychain from Kubernetes secrets (same as kubelet) - keychain, err := k8schain.NewInCluster(ctx, k8schain.Options{ - Namespace: e.namespace, - ServiceAccountName: e.serviceAccount, - }) - if err != nil { - return nil, fmt.Errorf("failed to create keychain: %w", err) - } - - // Get image digest for cache key - digest, err := crane.Digest(imageRef, crane.WithAuthFromKeychain(keychain)) - if err != nil { - return nil, fmt.Errorf("failed to resolve image digest: %w", err) - } - - // Check cache - if cached, ok := e.cache.Load(digest); ok { - return cached.(*BaseConfig), nil - } - - // Fetch config location from image labels - loc, err := e.getConfigLocation(imageRef, keychain) - if err != nil { - return nil, err - } - - var config *BaseConfig - - // Strategy 1: Inline base64 - if loc.Base64 != "" { - config, err = e.extractFromBase64(loc.Base64) - if err != nil { - return nil, err - } - } else if loc.LayerDigest != "" && loc.Path != "" { - // Strategy 2: Layer reference - config, err = e.extractFromLayer(ctx, imageRef, loc.LayerDigest, loc.Path, keychain) - if err != nil { - return nil, err - } - } else { - // No labels found - return nil, fmt.Errorf("distribution image %s missing config labels (io.llamastack.config.base64 or io.llamastack.config.layer)", imageRef) - } - - // Cache by digest - e.cache.Store(digest, config) - - return config, nil -} - -func (e *ImageConfigExtractor) getConfigLocation(imageRef string, kc authn.Keychain) (*ConfigLocation, error) { - configJSON, err := crane.Config(imageRef, crane.WithAuthFromKeychain(kc)) - if err != nil { - return nil, fmt.Errorf("failed to fetch image config: %w", err) - } - - var imgConfig v1.ConfigFile - if err := json.Unmarshal(configJSON, &imgConfig); err != nil { - return nil, err - } - - labels := imgConfig.Config.Labels - return &ConfigLocation{ - Base64: labels["io.llamastack.config.base64"], - LayerDigest: labels["io.llamastack.config.layer"], - Path: labels["io.llamastack.config.path"], - Version: labels["io.llamastack.config.version"], - }, nil -} - -func (e *ImageConfigExtractor) extractFromBase64(b64 string) (*BaseConfig, error) { - data, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - return nil, fmt.Errorf("invalid base64 config: %w", err) - } - - var config BaseConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("invalid config YAML: %w", err) - } - - return &config, nil -} - -func (e *ImageConfigExtractor) extractFromLayer( - ctx context.Context, - imageRef string, - layerDigest string, - path string, - kc authn.Keychain, -) (*BaseConfig, error) { - ref, err := name.ParseReference(imageRef) - if err != nil { - return nil, err - } - - // Fetch only the specific layer by digest - layerRef := ref.Context().Digest(layerDigest) - layer, err := remote.Layer(layerRef, remote.WithAuthFromKeychain(kc)) - if err != nil { - return nil, fmt.Errorf("failed to fetch layer %s: %w", layerDigest, err) - } - - reader, err := layer.Uncompressed() - if err != nil { - return nil, err - } - defer reader.Close() - - // Extract file from layer tar - tr := tar.NewReader(reader) - targetPath := strings.TrimPrefix(path, "/") - - for { - header, err := tr.Next() - if err == io.EOF { - return nil, fmt.Errorf("config file %s not found in layer", path) - } - if err != nil { - return nil, err - } - - if strings.TrimPrefix(header.Name, "./") == targetPath { - data, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - - var config BaseConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } - - return &config, nil - } - } -} -``` - -**Distribution Image Build Integration** (Phase 2): - -Labels are added post-build using `crane mutate` (solves the chicken-and-egg problem where layer digests are only known after build): - -```bash -#!/bin/bash -# build-distribution.sh - -IMAGE_REF="quay.io/llamastack/distribution-starter" -VERSION="${1:-latest}" -CONFIG_PATH="/app/config.yaml" -MAX_INLINE_SIZE=51200 # 50KB - -# Step 1: Build image normally -docker build -t "${IMAGE_REF}:build" . -docker push "${IMAGE_REF}:build" - -# Step 2: Extract config and determine strategy -CONFIG_DATA=$(crane export "${IMAGE_REF}:build" - | tar -xO "${CONFIG_PATH#/}" 2>/dev/null || echo "") -CONFIG_SIZE=${#CONFIG_DATA} - -# Step 3: Find layer containing config -LAYER_DIGEST="" -LAYERS=$(crane manifest "${IMAGE_REF}:build" | jq -r '.layers[].digest') -for layer in $LAYERS; do - if crane blob "${IMAGE_REF}@${layer}" | tar -tz 2>/dev/null | grep -q "${CONFIG_PATH#/}"; then - LAYER_DIGEST="$layer" - break - fi -done - -# Step 4: Add labels based on config size -if [ "$CONFIG_SIZE" -lt "$MAX_INLINE_SIZE" ]; then - CONFIG_B64=$(echo "$CONFIG_DATA" | base64 -w0) - crane mutate "${IMAGE_REF}:build" \ - --label "io.llamastack.config.base64=${CONFIG_B64}" \ - --label "io.llamastack.config.version=2" \ - -t "${IMAGE_REF}:${VERSION}" -else - crane mutate "${IMAGE_REF}:build" \ - --label "io.llamastack.config.layer=${LAYER_DIGEST}" \ - --label "io.llamastack.config.path=${CONFIG_PATH}" \ - --label "io.llamastack.config.version=2" \ - -t "${IMAGE_REF}:${VERSION}" -fi -``` - -**Key Points**: -- `crane mutate` updates only the config blob, not layers (layer digests unchanged) -- Labels added after build, so layer digest is known -- Works with any registry that supports OCI manifests - -**Air-Gapped / OpenShift Support** (Phase 2): - -The `k8schain` authenticator handles: -- imagePullSecrets from ServiceAccount -- ImageContentSourcePolicy (OpenShift image mirroring) -- Internal registry authentication - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Air-Gapped Cluster โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Operator โ”‚โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Internal Registry โ”‚ โ”‚ -โ”‚ โ”‚ (k8schain) โ”‚ โ”‚ (mirror.internal:5000) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ””โ”€ llamastack/dist-starter โ”‚ โ”‚ -โ”‚ โ–ผ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ฒ โ”‚ -โ”‚ โ”‚ ServiceAcct โ”‚โ”€โ”€imagePullSecretsโ”€โ”€โ”˜ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` + // +kubebuilder:validation:Required + Name string `json:"name"` + Provider string `json:"provider,omitempty"` + ContextLength *int `json:"contextLength,omitempty"` + ModelType string `json:"modelType,omitempty"` + Quantization string `json:"quantization,omitempty"` +} +``` + +**CEL validation rules** (on `LlamaStackDistributionSpec`): +1. `!(has(self.providers) && has(self.overrideConfig))` (mutual exclusivity) +2. `!(has(self.resources) && has(self.overrideConfig))` +3. `!(has(self.storage) && has(self.overrideConfig))` +4. `!(has(self.disabled) && has(self.overrideConfig))` +5. `!(has(self.distribution.name) && has(self.distribution.image))` (on DistributionSpec) +6. Per-type: `self.providers.inference.size() <= 1 || self.providers.inference.all(p, has(p.id))` +7. All provider IDs unique across all types +8. No provider type in both `providers` and `disabled` +9. TLS: `!self.networking.tls.enabled || has(self.networking.tls.secretName)` +10. Redis: `self.storage.kv.type != 'redis' || has(self.storage.kv.endpoint)` +11. Postgres: `self.storage.sql.type != 'postgres' || has(self.storage.sql.connectionString)` + +### 1.2 Config Generation Pipeline + +**Package**: `pkg/config/` + +The config generation pipeline is a pure function with no Kubernetes API calls: + +``` +Input: v1alpha2 Spec + base config ([]byte) + โ”‚ + โ”œโ”€โ”€ ParseBaseConfig(baseYAML) โ†’ map[string]interface{} + โ”‚ + โ”œโ”€โ”€ ExpandProviders(spec.Providers) โ†’ []configProvider + โ”‚ โ”œโ”€โ”€ Auto-generate IDs for single-element lists + โ”‚ โ”œโ”€โ”€ Add "remote::" prefix to provider_type + โ”‚ โ””โ”€โ”€ Map endpoint โ†’ config.url, settings โ†’ config.* + โ”‚ + โ”œโ”€โ”€ ExpandResources(spec.Resources, providers) โ†’ registeredResources + โ”‚ โ”œโ”€โ”€ Assign default provider for models without explicit provider + โ”‚ โ””โ”€โ”€ Validate provider references exist + โ”‚ + โ”œโ”€โ”€ ApplyStorage(spec.Storage, baseConfig) โ†’ updated config + โ”‚ + โ”œโ”€โ”€ ApplyDisabled(spec.Disabled, baseConfig) โ†’ updated config + โ”‚ + โ”œโ”€โ”€ CollectSecretRefs(spec) โ†’ []EnvVar + config substitutions + โ”‚ โ”œโ”€โ”€ From spec.Providers[*].APIKey + โ”‚ โ”œโ”€โ”€ From spec.Providers[*].SecretRefs + โ”‚ โ””โ”€โ”€ From spec.Storage (connectionString, password) + โ”‚ + โ”œโ”€โ”€ MergeConfig(baseConfig, userConfig) โ†’ merged config + โ”‚ โ”œโ”€โ”€ Providers: full API-type replacement + โ”‚ โ”œโ”€โ”€ Storage: per-subsection replacement + โ”‚ โ”œโ”€โ”€ Resources: additive + โ”‚ โ””โ”€โ”€ Disabled: subtractive + โ”‚ + โ””โ”€โ”€ SerializeConfig(merged) โ†’ YAML string + SHA-256 hash + +Output: GeneratedConfig { + ConfigYAML string + ContentHash string + EnvVars []corev1.EnvVar + ProviderCount int + ResourceCount int +} +``` + +### 1.3 ConfigResolver Interface + +**File**: `pkg/config/resolver.go` -**Pros**: -- Single-phase reconciliation (no two-phase complexity) -- Minimal network transfer (~10KB for manifest+config) -- Uses same auth as kubelet (imagePullSecrets work automatically) -- In-memory caching by digest (fast for repeated reconciles) - -**Cons**: -- Phase 2 requires distribution images to include config labels -- Requires `go-containerregistry` dependency -- Distribution build process must use `crane mutate` (Phase 2) - -#### 2.3 Implement Provider Expansion - -**File**: `pkg/config/provider.go` - -**Functionality**: -1. Parse polymorphic provider input (single vs list) -2. Auto-generate provider IDs for single providers -3. Map CRD fields to config.yaml format -4. Merge settings into provider config - -**Key functions**: -```go -func ExpandProviders(spec *v1alpha2.ProvidersSpec) ([]ProviderConfig, error) -func NormalizeProviderType(provider string) string // Add "remote::" prefix -func GenerateProviderID(providerType string) string -``` - -**Field mapping**: -| CRD Field | config.yaml Field | -|-----------|-------------------| -| provider | provider_type (with remote:: prefix) | -| endpoint | config.url | -| apiKey | config.api_key (via env var) | -| settings.* | config.* | - -#### 2.4 Implement Resource Expansion - -**File**: `pkg/config/resource.go` - -**Functionality**: -1. Parse polymorphic model input (string vs object) -2. Assign default provider for simple model strings -3. Generate registered_resources section - -**Key functions**: -```go -func ExpandResources(spec *v1alpha2.ResourcesSpec, providers []ProviderConfig) (*RegisteredResources, error) -func GetDefaultInferenceProvider(providers []ProviderConfig) string -``` - -#### 2.5 Implement Storage Configuration - -**File**: `pkg/config/storage.go` - -**Functionality**: -1. Map kv and sql storage specs to config.yaml format -2. Handle secret references for connection strings -3. Preserve distribution defaults when not specified - -**Key functions**: ```go -func ExpandStorage(spec *v1alpha2.StorageSpec, base *BaseConfig) (*StorageConfig, error) -``` - -#### 2.6 Implement Secret Resolution - -**File**: `pkg/config/secret_resolver.go` - -**Functionality**: -1. Collect all secretKeyRef references from the spec -2. Generate deterministic environment variable names -3. Create env var definitions for Deployment -4. Replace references with `${env.VAR_NAME}` in config - -**Key functions**: -```go -func ResolveSecrets(spec *v1alpha2.LlamaStackDistributionSpec) (*SecretResolution, error) - -type SecretResolution struct { - EnvVars []corev1.EnvVar // For Deployment - Substitutions map[string]string // Original -> ${env.VAR} +type ConfigResolver interface { + Resolve(distributionName string) ([]byte, error) } -``` - -**Naming convention**: `LLSD__` (e.g., `LLSD_INFERENCE_API_KEY`) -#### 2.7 Implement Config Generation Orchestration - -**File**: `pkg/config/config.go` - -**Functionality**: -1. Orchestrate the full config generation flow -2. Merge user config over base config -3. Apply disabled APIs -4. Generate final config.yaml content - -**Key functions**: -```go -func GenerateConfig(ctx context.Context, spec *v1alpha2.LlamaStackDistributionSpec, image string) (*GeneratedConfig, error) - -type GeneratedConfig struct { - ConfigYAML string // Final config.yaml content - EnvVars []corev1.EnvVar // Environment variables for secrets - ContentHash string // SHA256 of config content - ProviderCount int // For status reporting - ResourceCount int // For status reporting +type EmbeddedConfigResolver struct { + configs embed.FS // go:embed configs/ } -``` - -#### 2.8 Implement Version Detection - -**File**: `pkg/config/version.go` - -**Functionality**: -1. Detect config.yaml schema version from base config -2. Validate version is supported (n or n-1) -3. Return clear error for unsupported versions -**Key functions**: -```go -func DetectConfigVersion(config map[string]interface{}) (int, error) -func ValidateConfigVersion(version int) error -``` - -### Deliverables - -- [ ] `pkg/config/` package with all components -- [ ] Unit tests for each component (>80% coverage) -- [ ] Integration tests for full config generation -- [ ] Determinism tests (same input โ†’ same output) - ---- - -## Phase 3: Controller Integration - -**Goal**: Integrate config generation into the reconciliation loop. - -**Requirements Covered**: FR-023 to FR-026, FR-060 to FR-066, FR-073 to FR-075, FR-090 to FR-092 - -### Tasks - -#### 3.1 Create v1alpha2 Controller - -**File**: `controllers/llamastackdistribution_v1alpha2_controller.go` - -**Approach**: -- Extend existing controller to handle v1alpha2 -- Add config generation step in reconciliation -- Maintain compatibility with v1alpha1 flow - -**Reconciliation flow**: -``` -Reconcile() -โ”œโ”€โ”€ Fetch LLSD CR -โ”œโ”€โ”€ Validate (secrets, ConfigMaps exist) -โ”œโ”€โ”€ DetermineConfigSource() -โ”‚ โ”œโ”€โ”€ If overrideConfig โ†’ Use referenced ConfigMap -โ”‚ โ””โ”€โ”€ If providers/resources โ†’ GenerateConfig() -โ”œโ”€โ”€ ReconcileGeneratedConfigMap() -โ”œโ”€โ”€ ReconcileManifestResources() (existing) -โ”œโ”€โ”€ MergeExternalProviders() (spec 001 integration) -โ””โ”€โ”€ UpdateStatus() -``` - -#### 3.2 Implement Config Source Determination - -**Function**: `DetermineConfigSource()` - -**Logic**: -```go -func (r *Reconciler) DetermineConfigSource(instance *v1alpha2.LlamaStackDistribution) ConfigSource { - if instance.Spec.OverrideConfig != nil { - return ConfigSourceOverride - } - if instance.Spec.Providers != nil || instance.Spec.Resources != nil || instance.Spec.Storage != nil { - return ConfigSourceGenerated - } - return ConfigSourceDistributionDefault +func (r *EmbeddedConfigResolver) Resolve(name string) ([]byte, error) { + return r.configs.ReadFile(fmt.Sprintf("configs/%s/config.yaml", name)) } ``` -#### 3.3 Implement Generated ConfigMap Reconciliation - -**Function**: `ReconcileGeneratedConfigMap()` - -**Logic**: -1. Call `pkg/config.GenerateConfig()` to generate config -2. Create ConfigMap with hash-based name: `{name}-config-{hash[:8]}` -3. Set owner reference for garbage collection -4. Clean up old ConfigMaps (keep last 2) +Phase 2 adds `OCIConfigResolver` implementing the same interface. -**Key considerations**: -- Immutable ConfigMaps (create new, don't update) -- Content hash ensures change detection -- Owner references enable automatic cleanup +### 1.4 Controller Integration -#### 3.4 Extend ManifestContext +**Modified**: `controllers/llamastackdistribution_controller.go` -**File**: `pkg/deploy/kustomizer.go` - -**Additions to ManifestContext**: -```go -type ManifestContext struct { - // Existing fields... +The reconciliation flow adds v1alpha2 config generation between distribution resolution and Deployment creation: - // New fields for v1alpha2 - GeneratedConfigMapName string - GeneratedConfigHash string - SecretEnvVars []corev1.EnvVar -} ``` - -#### 3.5 Implement Networking Configuration - -**Functions**: -- `ReconcileNetworking()`: Handle port, TLS, expose -- Extend existing Ingress reconciliation for polymorphic expose - -**Logic for polymorphic expose**: -```go -func (r *Reconciler) ShouldExposeRoute(spec *v1alpha2.NetworkingSpec) bool { - if spec == nil || spec.Expose == nil { - return false - } - if spec.Expose.Enabled != nil { - return *spec.Expose.Enabled - } - // expose: {} (non-nil pointer, all zero-valued fields) is treated as - // expose: true per edge case "Polymorphic expose with empty object" - return true -} +Reconcile(ctx, req) + โ”‚ + โ”œโ”€โ”€ Fetch LLSD CR (v1alpha2 - hub version) + โ”‚ + โ”œโ”€โ”€ Resolve distribution (name โ†’ image via distributions.json + overrides) + โ”‚ + โ”œโ”€โ”€ Determine config path: + โ”‚ โ”œโ”€โ”€ overrideConfig? โ†’ Use referenced ConfigMap directly + โ”‚ โ”œโ”€โ”€ providers/resources/storage? โ†’ Generate config + โ”‚ โ””โ”€โ”€ Neither? โ†’ Use embedded default config unchanged + โ”‚ + โ”œโ”€โ”€ If generating: + โ”‚ โ”œโ”€โ”€ Resolve base config via ConfigResolver + โ”‚ โ”œโ”€โ”€ Call pkg/config.GenerateConfig(spec, baseConfig) + โ”‚ โ”œโ”€โ”€ Compare content hash with current ConfigMap + โ”‚ โ”œโ”€โ”€ If changed: create new ConfigMap, update Deployment atomically + โ”‚ โ”œโ”€โ”€ If unchanged: skip (no-op, no Pod restart) + โ”‚ โ””โ”€โ”€ Clean up old ConfigMaps (retain last 2) + โ”‚ + โ”œโ”€โ”€ Update status conditions + emit Events + โ”‚ + โ””โ”€โ”€ On error: preserve current Deployment, set ConfigGenerated=False ``` -#### 3.6 Implement Validation +### 1.5 Validating Webhook -**Functions**: -- `ValidateSecretReferences()`: Verify all secretKeyRefs exist -- `ValidateConfigMapReferences()`: Verify overrideConfig and caBundle exist -- `ValidateProviderReferences()`: Verify model โ†’ provider references - -**Error message format**: -``` -Secret "vllm-creds" not found in namespace "default". -Referenced by: spec.providers.inference.apiKey.secretKeyRef -``` +**File**: `api/v1alpha2/llamastackdistribution_webhook.go` -#### 3.7 Implement Spec 001 Integration +Validates at admission time: +- Distribution name exists in distributions.json (FR-079d) +- Referenced Secrets exist (FR-073) +- Referenced ConfigMaps exist for overrideConfig and caBundle (FR-074) +- Provider ID references in resources.models[].provider are valid (FR-041) +- No provider type appears in both providers and disabled -**Function**: `MergeExternalProviders()` +### 1.6 Conversion Webhook -**Logic**: -1. Get generated config (base) -2. Apply external providers from spec 001 -3. Log warning on ID conflicts -4. External providers override inline providers +**Files**: `api/v1alpha1/llamastackdistribution_conversion.go`, `api/v1alpha2/llamastackdistribution_conversion.go` -#### 3.8 Extend Status Reporting +- v1alpha2 is the hub (storage version) +- v1alpha1 is the spoke with `ConvertTo` and `ConvertFrom` methods +- v1alpha2-only fields stored as JSON annotation `llamastack.io/v1alpha2-fields` for round-trip fidelity +- v1alpha1 fields map to v1alpha2 per the migration table in spec.md -**New status fields**: -```go -type ConfigGenerationStatus struct { - ConfigMapName string `json:"configMapName,omitempty"` - GeneratedAt metav1.Time `json:"generatedAt,omitempty"` - ProviderCount int `json:"providerCount,omitempty"` - ResourceCount int `json:"resourceCount,omitempty"` - ConfigVersion int `json:"configVersion,omitempty"` -} -``` +### 1.7 Kustomize / Deployment -**New conditions**: -- `ConfigGenerated`: True when config successfully generated -- `SecretsResolved`: True when all secret references valid +**New directories**: `config/certmanager/`, `config/openshift/` -### Deliverables - -- [ ] Extended controller with v1alpha2 support -- [ ] Config generation integration -- [ ] Networking configuration -- [ ] Validation with actionable errors -- [ ] Spec 001 integration -- [ ] Status reporting extensions - ---- - -## Phase 4: Conversion Webhook - -**Goal**: Enable backward compatibility between v1alpha1 and v1alpha2. - -**Requirements Covered**: FR-080 to FR-083 - -### Tasks - -#### 4.1 Implement Conversion Hub - -**File**: `api/v1alpha2/llamastackdistribution_conversion.go` - -**Approach**: v1alpha2 is the hub (storage version). In controller-runtime, the Hub type only implements a `Hub()` marker method. Conversion logic (`ConvertTo`/`ConvertFrom`) lives on the Spoke (v1alpha1). - -```go -// Hub marks v1alpha2 as the storage version for conversion. -// The Hub interface requires only this marker method. -// All conversion logic is implemented on the v1alpha1 spoke. -func (dst *LlamaStackDistribution) Hub() {} -``` - -#### 4.2 Implement v1alpha1 Spoke Conversion - -**File**: `api/v1alpha1/llamastackdistribution_conversion.go` - -**v1alpha1 โ†’ v1alpha2**: -```go -func (src *LlamaStackDistribution) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1alpha2.LlamaStackDistribution) - - // Map fields according to migration table - dst.Spec.Distribution = convertDistribution(src.Spec.Server.Distribution) - dst.Spec.Networking = convertNetworking(src.Spec.Server, src.Spec.Network) - dst.Spec.Workload = convertWorkload(src.Spec) - dst.Spec.OverrideConfig = convertUserConfig(src.Spec.Server.UserConfig) - // etc. - - return nil -} -``` - -**v1alpha2 โ†’ v1alpha1**: -```go -func (dst *LlamaStackDistribution) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1alpha2.LlamaStackDistribution) - - // Reverse mapping - dst.Spec.Server.Distribution = convertDistributionBack(src.Spec.Distribution) - // etc. - - // Note: New fields (providers, resources, storage) cannot be represented in v1alpha1 - // These are lost in down-conversion - - return nil -} -``` - -#### 4.3 Configure Webhook - -**File**: `config/webhook/manifests.yaml` - -**Enable conversion webhook**: -```yaml -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: llamastackdistributions.llamastack.io -spec: - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1"] - clientConfig: - service: - namespace: system - name: webhook-service - path: /convert -``` - -#### 4.4 Register Webhook in Main - -**File**: `main.go` - -```go -if err = (&llamastackv1alpha1.LlamaStackDistribution{}).SetupWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "LlamaStackDistribution") - os.Exit(1) -} -``` - -### Deliverables - -- [ ] Conversion webhook implementation -- [ ] v1alpha1 โ†” v1alpha2 field mapping -- [ ] Webhook configuration -- [ ] Conversion tests - ---- - -## Phase 5: Testing & Documentation - -**Goal**: Ensure quality and provide user guidance. - -**Requirements Covered**: All NFRs, User Stories - -### Tasks - -#### 5.1 Unit Tests - -**Coverage targets**: -- `pkg/config/`: >80% -- `api/v1alpha2/`: >80% -- Conversion logic: 100% - -**Test files**: -- `pkg/config/config_test.go` -- `pkg/config/provider_test.go` -- `pkg/config/resource_test.go` -- `api/v1alpha2/conversion_test.go` - -#### 5.2 Integration Tests - -**Test scenarios** (from spec user stories): -1. Simple inference configuration -2. Multiple providers -3. Resource registration -4. State storage configuration -5. Network exposure -6. Override config -7. v1alpha1 migration - -**Test file**: `controllers/llamastackdistribution_v1alpha2_test.go` - -#### 5.3 E2E Tests - -**Test scenarios**: -1. Deploy LLSD with generated config, verify server starts -2. Update provider config, verify rolling update -3. Migration from v1alpha1 to v1alpha2 - -**Test file**: `tests/e2e/config_generation_test.go` - -#### 5.4 Sample Manifests - -**Files to create**: -- `config/samples/v1alpha2-simple.yaml` -- `config/samples/v1alpha2-full.yaml` -- `config/samples/v1alpha2-postgres.yaml` -- `config/samples/v1alpha2-multi-provider.yaml` - -#### 5.5 Documentation - -**Files to update**: -- `README.md`: Add v1alpha2 overview -- `docs/configuration.md`: Detailed configuration guide -- `docs/migration-v1alpha1-to-v1alpha2.md`: Migration guide - -### Deliverables - -- [ ] Unit tests with >80% coverage -- [ ] Integration tests for all user stories -- [ ] E2E tests -- [ ] Sample manifests -- [ ] Documentation - ---- +- cert-manager for webhook TLS certificate management +- OpenShift overlay with service-ca annotation for webhook certificates +- CRD patches for conversion webhook endpoint +- Manager deployment patch for webhook port and certificate volume ## Implementation Order ``` -Week 1-2: Phase 1 (CRD Schema) - โ””โ”€โ”€ Foundation for all other phases - -Week 3-4: Phase 2 (Config Generation Engine) - โ””โ”€โ”€ Core logic, can be tested independently - -Week 5-6: Phase 3 (Controller Integration) - โ””โ”€โ”€ Depends on Phase 1 and 2 - -Week 7: Phase 4 (Conversion Webhook) - โ””โ”€โ”€ Depends on Phase 1 - -Week 8: Phase 5 (Testing & Documentation) - โ””โ”€โ”€ Depends on all phases +Phase 1: CRD Types + 1.1 v1alpha2 types (all structs, CEL rules, deepcopy) + 1.2 Generate CRD manifests (controller-gen) + +Phase 2: Config Generation (pkg/config/) + 2.1 ConfigResolver interface + EmbeddedConfigResolver + 2.2 Provider expansion + 2.3 Resource expansion + 2.4 Storage configuration + 2.5 Secret reference collection + 2.6 Config merging + 2.7 Generator orchestrator + 2.8 Unit tests for all of the above + +Phase 3: Controller Integration + 3.1 ConfigMap reconciler (create, hash compare, cleanup) + 3.2 v1alpha2 controller helpers (config path routing) + 3.3 Modify reconcile loop for v1alpha2 support + 3.4 Networking overrides (port from spec) + 3.5 Events emission (NFR-007) + 3.6 Status condition wiring (all 4 conditions) + 3.7 Controller tests + +Phase 4: Webhooks + 4.1 Validating webhook + 4.2 Conversion webhook (v1alpha1 โ†” v1alpha2) + 4.3 Webhook tests + 4.4 Kustomize deployment manifests + +Phase 5: Testing & Docs + 5.1 E2E tests for v1alpha2 + 5.2 v1alpha2 sample CRs + 5.3 Migration documentation ``` ---- - ## Risk Mitigation | Risk | Mitigation | -|------|------------| -| Polymorphic JSON parsing complexity | Use json.RawMessage with well-tested parsing functions | -| Config extraction from images | OCI label approach with k8schain auth; clear error message guides users to use `overrideConfig` as fallback when labels missing | -| Registry authentication failures | Use k8schain (same auth as kubelet); respects imagePullSecrets automatically | -| Conversion webhook failures | Comprehensive unit tests, fallback to direct storage access | -| Breaking changes in config.yaml schema | Version detection and n-1 support | - ---- - -## Success Criteria - -- [ ] All FR requirements implemented and tested -- [ ] All NFR requirements met -- [ ] All user stories have passing integration tests -- [ ] v1alpha1 CRs continue to work after upgrade -- [ ] Documentation complete and reviewed -- [ ] No regression in existing functionality - ---- - -## References - -- Spec: `specs/002-operator-generated-config/spec.md` -- Design Doc: [LlamaStackDistribution CRD v1alpha2 Schema Design](https://docs.google.com/document/d/10VhoQPb8bLGUo9yka4MXuEGIZClGf1oBr31TpK4NLD0/edit) -- Constitution: `specs/constitution.md` -- Related Spec: `specs/001-deploy-time-providers-l1/spec.md` +|------|-----------| +| Large PR blocks review | Split into 4-5 focused PRs following the phase order | +| CEL rules on complex cross-type validation | Validate with `controller-gen` during Phase 1.2 before writing config generation code | +| Conversion webhook data loss | Round-trip tests with annotation preservation in Phase 4.2 | +| Config generation determinism | Table-driven tests with golden file comparison in Phase 2.8 | +| Webhook availability on operator startup | cert-manager handles certificate lifecycle; readiness probe gates webhook | diff --git a/specs/002-operator-generated-config/quickstart.md b/specs/002-operator-generated-config/quickstart.md index e18ed9e57..4c98f42ed 100644 --- a/specs/002-operator-generated-config/quickstart.md +++ b/specs/002-operator-generated-config/quickstart.md @@ -22,8 +22,8 @@ spec: name: starter providers: inference: - provider: vllm - endpoint: "http://vllm-service:8000" + - provider: vllm + endpoint: "http://vllm-service:8000" ``` Apply and verify: @@ -60,12 +60,12 @@ spec: name: remote-vllm providers: inference: - provider: vllm - endpoint: "https://vllm.example.com" - apiKey: - secretKeyRef: - name: vllm-creds - key: token + - provider: vllm + endpoint: "https://vllm.example.com" + apiKey: + secretKeyRef: + name: vllm-creds + key: token ``` The operator resolves the secret reference to an environment variable (`LLSD_VLLM_API_KEY`) and injects it into the Deployment. The secret value never appears in the ConfigMap. @@ -116,7 +116,7 @@ spec: endpoint: "http://vllm-secondary:8000" resources: models: - - "llama3.2-8b" # Uses first inference provider + - name: "llama3.2-8b" # Uses first inference provider - name: "llama3.2-70b" # Uses specified provider provider: vllm-secondary contextLength: 128000 @@ -127,7 +127,7 @@ spec: - llama-guard ``` -Simple model strings (e.g., `"llama3.2-8b"`) are registered with the first inference provider. Use the object form to assign models to specific providers. +When `provider` is omitted, models are registered with the first inference provider. Specify `provider` to assign models to specific providers. ## Example 5: PostgreSQL State Storage @@ -149,8 +149,8 @@ spec: name: starter providers: inference: - provider: vllm - endpoint: "http://vllm:8000" + - provider: vllm + endpoint: "http://vllm:8000" storage: sql: type: postgres @@ -183,11 +183,11 @@ spec: apiKey: secretKeyRef: {name: vllm-creds, key: token} safety: - provider: llama-guard + - provider: llama-guard resources: models: - - "llama3.2-8b" + - name: "llama3.2-8b" shields: - llama-guard @@ -207,6 +207,7 @@ spec: enabled: true secretName: llama-tls expose: + enabled: true hostname: "llama.example.com" allowedFrom: namespaces: ["app-ns"] @@ -274,7 +275,7 @@ kubectl patch llsd my-stack --type merge -p ' spec: providers: safety: - provider: llama-guard + - provider: llama-guard ' # Watch the rollout diff --git a/specs/002-operator-generated-config/research.md b/specs/002-operator-generated-config/research.md index 132d0d763..3964056fb 100644 --- a/specs/002-operator-generated-config/research.md +++ b/specs/002-operator-generated-config/research.md @@ -19,40 +19,46 @@ ## Research Areas -### R1: Polymorphic JSON Parsing in Go CRD Types +### R1: Provider and Resource Type Design -**Decision**: Use `json.RawMessage` with custom `UnmarshalJSON` methods for polymorphic fields (single object vs list). +**Decision**: Use typed `[]ProviderConfig` slices for all provider fields. A single provider is expressed as a one-element list. Use `[]ModelConfig` (with only `name` required) for models. No polymorphic types. -**Rationale**: Kubebuilder CRDs cannot natively express "object OR array" in OpenAPI v3 schema. Using `json.RawMessage` allows runtime parsing while keeping the CRD schema flexible via `// +kubebuilder:validation:Type=object` or `apiextensionsv1.JSON`. +**Rationale** (revised after PR #253 review): +The original design used `json.RawMessage` / `apiextensionsv1.JSON` for polymorphic fields (single object OR list). PR #253 review revealed this creates several problems: +1. Kubebuilder validation markers don't apply to opaque JSON fields +2. CEL rules (FR-071, FR-072) cannot inspect opaque JSON, making provider ID validation impossible at admission time +3. ~500 lines of parsing code needed (`ParsePolymorphicProvider`, `collectSecretRefsFromProviderField`, etc.) +4. Heuristic secret detection (`extractDirectSecretRef` matching any map with `name`+`key`) produces false positives +5. Contradicts constitution ยง2.1 ("MUST use kubebuilder validation tags") + +**Trade-off**: Users write `- provider: vllm` (list syntax) instead of `provider: vllm` (object syntax). This is a one-character YAML difference per provider, versus eliminating all parsing complexity and enabling full CRD validation. **Alternatives considered**: -- `apiextensionsv1.JSON` (used in v1alpha1 for `ProviderInfo.Config`): Works but loses schema validation. Good for escape hatches like `settings`, not ideal for structured polymorphic types. -- Custom OpenAPI schema markers: Too complex for multi-form types. Kubebuilder does not support oneOf natively. -- Separate fields (`InferenceProvider` + `InferenceProviders`): Verbose, poor UX. Users would need to choose the correct field based on provider count. +- `json.RawMessage` with custom unmarshaling (original design): Rejected due to above issues +- `apiextensionsv1.JSON`: Same problems, plus no IDE autocompletion +- Separate fields (`InferenceProvider` + `InferenceProviders`): Verbose, users must choose correct field **Implementation pattern**: ```go -type ProviderConfigOrList struct { - raw json.RawMessage -} - -func (p *ProviderConfigOrList) UnmarshalJSON(data []byte) error { - p.raw = data - return nil +type ProvidersSpec struct { + Inference []ProviderConfig `json:"inference,omitempty"` + Safety []ProviderConfig `json:"safety,omitempty"` + VectorIo []ProviderConfig `json:"vectorIo,omitempty"` + ToolRuntime []ProviderConfig `json:"toolRuntime,omitempty"` + Telemetry []ProviderConfig `json:"telemetry,omitempty"` } -func (p *ProviderConfigOrList) Resolve() ([]ProviderConfig, error) { - // Try single object first, then list - var single ProviderConfig - if err := json.Unmarshal(p.raw, &single); err == nil { - return []ProviderConfig{single}, nil - } - var list []ProviderConfig - return list, json.Unmarshal(p.raw, &list) +type ProviderConfig struct { + ID string `json:"id,omitempty"` + Provider string `json:"provider"` + Endpoint string `json:"endpoint,omitempty"` + APIKey *SecretKeyRef `json:"apiKey,omitempty"` + SecretRefs map[string]SecretKeyRef `json:"secretRefs,omitempty"` + Settings apiextensionsv1.JSON `json:"settings,omitempty"` } ``` -**Risk**: CRD OpenAPI schema will show `type: object` without detailed subschema for the polymorphic fields. Users rely on documentation and examples rather than schema-driven editor completion for these fields. +**Risk**: Low. The only UX cost is requiring list syntax for single providers. --- @@ -213,7 +219,7 @@ The resolution chain is: | Area | Decision | Risk Level | |------|----------|------------| -| Polymorphic JSON | `json.RawMessage` with custom unmarshaling | Medium (limited schema validation) | +| Provider/resource types | Typed `[]ProviderConfig` slices, `[]ModelConfig` | Low (full schema validation) | | Base config source | Embedded `go:embed` (Phase 1) + OCI labels (Phase 2) | Low | | Config merging | Deep merge with provider replacement semantics | Low | | Env var naming | `LLSD__` | Low | diff --git a/specs/002-operator-generated-config/review_summary.md b/specs/002-operator-generated-config/review_summary.md index 3db1dd567..7485d1e34 100644 --- a/specs/002-operator-generated-config/review_summary.md +++ b/specs/002-operator-generated-config/review_summary.md @@ -1,189 +1,126 @@ -# Spec Brief: Operator-Generated Config (v1alpha2) - -**Full spec:** [spec.md](spec.md) | **Status:** Draft | **Priority:** P1 - -## Problem Statement - -Users currently must provide a complete `config.yaml` via ConfigMap to configure LlamaStack. This requires deep knowledge of the config schema and results in verbose, error-prone YAML. - -## Solution - -Introduce v1alpha2 API with high-level abstractions that the operator expands into a complete `config.yaml`. Users write 10-20 lines instead of 200+. - -## Before/After Example - -**Before (v1alpha1):** User provides 200+ line ConfigMap manually - -**After (v1alpha2):** -```yaml -apiVersion: llamastack.io/v1alpha2 -kind: LlamaStackDistribution -metadata: - name: my-stack -spec: - distribution: - name: starter - providers: - inference: - provider: vllm - endpoint: "http://vllm:8000" - apiKey: - secretKeyRef: {name: vllm-creds, key: token} - resources: - models: ["llama3.2-8b"] - storage: - sql: - type: postgres - connectionString: - secretKeyRef: {name: pg-creds, key: url} -``` +# Review Summary: Operator-Generated Server Configuration (v1alpha2) -## Key Design Decisions +**Feature**: 002-operator-generated-config +**Branch**: 002-reimpl +**Review Date**: 2026-03-10 -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Config extraction | OCI image labels | Single-phase reconcile, works with imagePullSecrets | -| Secret handling | Environment variables | Never embed secrets in ConfigMap | -| Multiple providers | Explicit `id` required for providers 2..N | Avoid ambiguity in provider references | -| Backward compat | Conversion webhook | v1alpha1 CRs continue working | -| Override escape hatch | `overrideConfig` field | Power users can bypass generation | +## How to Review This Spec (30-Minute Recipe) -## Configuration Tiers +This is a spec-only PR. No implementation code. The goal is to validate the design before writing code, so we catch issues like the polymorphism problem from PR #253 early. -| Tier | Mechanism | -|------|-----------| -| Simple (80%) | Inline provider fields | -| Advanced (15%) | Per-provider `settings` | -| Full Control (5%) | ConfigMap override | +### Step 1: Understand the Goal (3 min) -## New Spec Sections +Read the **Purpose** and **Configuration Tiers** table in `spec.md` (lines 9-11, 581-583). The core idea: users write 10-20 lines of YAML instead of a 200-line ConfigMap. Three tiers: simple inline, advanced settings, full override. -``` -spec: - distribution: # Image source (name or direct image) - providers: # Inference, safety, vectorIo, toolRuntime, telemetry - resources: # Models, tools, shields to register - storage: # KV (sqlite/redis) and SQL (sqlite/postgres) - disabled: # APIs to disable - networking: # Port, TLS, expose, allowedFrom - workload: # Replicas, resources, autoscaling, PDB - overrideConfig: # Escape hatch: use ConfigMap directly -``` +### Step 2: Review the CRD Example (5 min) -## What Reviewers Should Focus On +Read the **Complete v1alpha2 Spec Structure** YAML in `spec.md` (lines 379-473). This is what users will write. Ask yourself: -1. **API Design**: Does the field structure make sense? Any awkward names? -2. **Polymorphic Fields**: Single object vs list forms (providers, models) -3. **Storage Abstraction**: Is kv/sql split intuitive? -4. **Edge Cases**: Are the 12 documented edge cases reasonable? -5. **Phased Base Config**: Is the embedded configs (Phase 1) + OCI labels (Phase 2) approach acceptable? Any better idea how to extract the `config.yaml` from the distribution OCI image? -6. **OQ-004**: Should the operator auto-create a default LLSD instance on install? +- Does this YAML feel natural for a Kubernetes user? +- Are the field names intuitive? +- Would you know what to write without reading docs? -## Requirements Summary +Key design decision to validate: **providers are always lists** (e.g., `inference: [{provider: vllm}]` not `inference: {provider: vllm}`). We chose this to enable kubebuilder validation and CEL rules, at the cost of slightly more verbose YAML for single providers. If you disagree, this is the most impactful thing to flag. -| Category | Count | Coverage | -|----------|-------|----------| -| CRD Schema | FR-001 to FR-014 | All new fields defined | -| Config Generation | FR-020 to FR-029 | Extraction, merging, versioning | -| Providers | FR-030 to FR-035 | Field mapping, ID generation | -| Resources | FR-040 to FR-044 | Models, tools, shields | -| Storage | FR-050 to FR-053 | KV and SQL backends | -| Networking | FR-060 to FR-066 | Port, TLS, expose, NetworkPolicy | -| Validation | FR-070 to FR-075 | CEL rules, secret/ConfigMap checks | -| Conversion | FR-080 to FR-083 | v1alpha1 โ†” v1alpha2 webhook | -| Integration | FR-090 to FR-092 | Spec 001 external providers | +### Step 3: Check the Three Critical Design Decisions (10 min) -## User Stories (P1 only) +These are the decisions that will be hardest to change after implementation starts: -1. **Simple Inference**: Deploy with just `providers.inference` config -2. **Multiple Providers**: Configure primary + fallback providers -3. **Resource Registration**: Register models/tools declaratively -4. **State Storage**: Configure PostgreSQL for persistence +**Decision 1: Typed slices instead of polymorphic JSON** (spec.md FR-004, research.md R1) -## Dependencies +The original PR #253 used `apiextensionsv1.JSON` for polymorphic fields (object OR list). This caused: no kubebuilder validation, impossible CEL rules, ~500 lines of parsing code, false-positive secret detection bugs. The new design uses `[]ProviderConfig` everywhere. Tradeoff: users always write list syntax. Verify this is acceptable. -- **Spec 001**: External providers merge into generated config (not mandatory, but was already included in this design) -- **Distribution images**: Must include OCI labels with base config (check: build system must support this, registry must support label queries (check for disconnected)) +**Decision 2: Explicit `secretRefs` field instead of heuristic detection** (spec.md FR-005, contracts/config-generation.yaml) -## Open Questions +The original PR #253 scanned the `settings` map for any `{name, key}` structure and treated it as a Secret reference. This caused false positives. The new design adds an explicit `secretRefs: map[string]SecretKeyRef` field on ProviderConfig. The `settings` map is passed through without any secret resolution. Check `contracts/config-generation.yaml` "Secret Resolution" section for examples. -Previously open questions (OQ-001 through OQ-003) have been resolved: +**Decision 3: Provider merge = full API-type replacement** (contracts/config-generation.yaml merge_rules) -- **OQ-001** (Resolved): `expose: {}` is treated as `expose: true` -- **OQ-002** (Resolved): Disabled API + provider config conflict produces a warning (not error). Disabled takes precedence. -- **OQ-003** (Resolved): Env var naming uses provider ID: `LLSD__` (unique, collision-free) +When a user specifies `providers.inference`, ALL base config inference providers are replaced. Base providers with unmatched IDs are dropped. The contract has before/after examples. Verify this matches your expectation. The alternative (merge-by-ID, preserving unmatched base providers) was the original PR #253 behavior but contradicted the contract. -Currently open: +### Step 4: Spot-Check CEL Validation Rules (5 min) -- **OQ-004**: Should the operator create a default LlamaStackDistribution instance when installed? If adopted, it should be opt-in via operator configuration (e.g., a Helm value or OLM parameter). +Read `contracts/crd-schema.yaml` CEL rules section (bottom of file). There are 11 rules. Focus on: -## Implementation Estimate +- **Rule 6** (provider ID required when list > 1): Can CEL express `self.providers.inference.size() <= 1 || self.providers.inference.all(p, has(p.id))`? This is the rule that was impossible with JSON types. +- **Rule 8** (disabled + provider conflict): Should this be an error or a warning? We chose error. If you prefer warning, flag it. +- **Rules 9-11** (conditional fields): TLS needs secretName when enabled, Redis needs endpoint, Postgres needs connectionString. These were missing in the original spec. -5 phases, 38 tasks (see [tasks.md](tasks.md) for details) +### Step 5: Scan Edge Cases (4 min) ---- +Read the **Edge Cases** section in `spec.md` (lines 130-176). There are 13 edge cases. Focus on the ones that affect data integrity: -## Changes Since Initial Spec (2026-02-10) +- "Secret references via settings vs secretRefs": confirms settings map is never inspected for secrets +- "Disabled APIs conflict with providers": now an error, not a warning +- "Config generation failure on update": preserves running Deployment, critical for production -The following spec.md updates were applied after a cross-artifact consistency analysis. These are additive refinements, not structural redesigns. +### Step 6: Verify Conversion Strategy (3 min) -### New: User Story 8 (Runtime Configuration Updates, P1) +Read the **Field Mapping: v1alpha1 to v1alpha2** table in `spec.md` (lines 477-499). Check that existing v1alpha1 fields all have a v1alpha2 home. New v1alpha2-only fields (providers, resources, storage, disabled) are stored as JSON annotation for round-trip fidelity. This is the standard kubebuilder pattern. -Covers day-2 operations: CR updates trigger config regeneration, no-op detection (skip restart when config unchanged), failure preservation (current Deployment kept running on error), and atomic image+config updates on distribution changes. +### What NOT to Review -### New: Phased Base Config Extraction (FR-027a to FR-027j) +- `plan.md`: Implementation details, will change during coding +- `tasks.md`: Task breakdown, auto-generated, will evolve +- `data-model.md`: Derived from spec, no independent decisions +- `quickstart.md`: Examples only, validated against CRD schema -Replaced the single "OCI label extraction" approach with a two-phase strategy: -- **Phase 1 (MVP)**: Embedded default configs via `go:embed`, no distribution image changes needed -- **Phase 2 (Enhancement)**: OCI label extraction takes precedence when labels are present +## Key Changes from PR #253 -This introduces new **Operator Build Requirements**: the operator binary must ship with `distributions.json` (mapping distribution names to image references) and a `configs//config.yaml` for each named distribution. These are maintained together and updated as part of the operator release process. Downstream builds (e.g., RHOAI) use the existing `image-overrides` mechanism to remap image references without rebuilding the operator. +This spec addresses all critical issues raised in the PR #253 review: -### New: Runtime Configuration Requirements (FR-095 to FR-101) +| PR #253 Issue | Resolution | Spec Location | +|--------------|------------|---------------| +| Polymorphic JSON types lose kubebuilder validation | Replaced with typed `[]ProviderConfig` slices | FR-004, research.md R1 | +| CEL rules impossible on `apiextensionsv1.JSON` | CEL now works because providers are typed | FR-071, FR-072 | +| `extractDirectSecretRef` false positives | Explicit `secretRefs` field, no heuristic matching | FR-005 | +| `sortedMapKeys` doesn't sort | Determinism addressed in NFR-001, merge.go | NFR-001 | +| Missing CEL for TLS/storage conditionals | Added FR-079, FR-079a-c | Validation section | +| Disabled + provider should be error not warning | Changed to validation error | OQ-002, edge case | +| Status conditions defined but unwired | Tasks T036, T055, T090 wire all 4 conditions | tasks.md | +| Missing test coverage for FR-097, FR-096, FR-100 | Dedicated tasks T053-T058 | tasks.md Phase 7 | +| Contract says replace but code does merge-by-ID | Contract updated with explicit examples | config-generation.yaml | -- **FR-095-096**: Regenerate on spec change; skip restart when content hash is identical -- **FR-097**: On failure, preserve the current running Deployment unchanged -- **FR-098-099**: Atomic updates when distribution changes; status conditions reflect update state -- **FR-100**: Image + config updated in a single Deployment update (no intermediate mismatch) -- **FR-101**: Operator upgrade failure handling with `UpgradeConfigFailure` reason +## Coverage Matrix -### New: Validation Webhook Requirements (FR-076 to FR-078) +| Spec Requirement | Plan Section | Task(s) | Status | +|-----------------|-------------|---------|--------| +| FR-001 v1alpha2 API version | 1.1 CRD Schema | T001-T002 | Covered | +| FR-002 Distribution name/image | 1.1 CRD Schema | T003 | Covered | +| FR-003 Provider types | 1.1 CRD Schema | T004 | Covered | +| FR-004 Typed provider slices | 1.1 CRD Schema | T004 | Covered | +| FR-005 ProviderConfig fields + secretRefs | 1.1 CRD Schema | T004-T005 | Covered | +| FR-006-007 Resources (ModelConfig) | 1.1 CRD Schema | T006 | Covered | +| FR-008 Storage subsections | 1.1 CRD Schema | T007 | Covered | +| FR-010-011 Networking + ExposeConfig | 1.1 CRD Schema | T008 | Covered | +| FR-012 WorkloadSpec | 1.1 CRD Schema | T009 | Covered | +| FR-013 OverrideConfig mutual exclusivity | 1.1 CEL rules | T010, T012 | Covered | +| FR-020 Distribution resolution | 1.4 Controller | T033 | Covered | +| FR-021 Config generation | 1.2 Pipeline | T023 | Covered | +| FR-022 SecretKeyRef resolution | 1.2 Pipeline | T020 | Covered | +| FR-023-025 ConfigMap creation + owner ref | 1.4 Controller | T031-T032 | Covered | +| FR-025a ConfigMap cleanup (retain 2) | 1.4 Controller | T032 | Covered | +| FR-027a1 ConfigResolver interface | 1.3 ConfigResolver | T014 | Covered | +| FR-027a-e Embedded configs | 1.3 ConfigResolver | T014-T016 | Covered | +| FR-030-035 Provider field mapping | 1.2 Pipeline | T019 | Covered | +| FR-040-044 Resource registration | 1.2 Pipeline | T021, T045-T048 | Covered | +| FR-050-053 Storage configuration | 1.2 Pipeline | T022, T049-T052 | Covered | +| FR-060-066 Networking | 1.4 Controller | T059-T063 | Covered | +| FR-070-072 CEL validation | 1.1 CEL rules | T012 | Covered | +| FR-073-078 Webhook validation | 1.5 Webhook | T074-T080 | Covered | +| FR-079-079d Conditional CEL + webhook | 1.1/1.5 | T012, T074 | Covered | +| FR-080-083 Conversion webhook | 1.6 Conversion | T068-T073 | Covered | +| FR-095-098 Runtime updates | 1.4 Controller | T053-T058 | Covered | +| FR-099 Status conditions | 1.4 Controller | T036, T055, T090 | Covered | +| FR-100 Atomic deployment update | 1.4 Controller | T035 | Covered | +| NFR-001 Deterministic output | 1.2 Pipeline | T030 | Covered | +| NFR-005 Immutable ConfigMaps | 1.4 Controller | T031 | Covered | +| NFR-007 Kubernetes Events | 1.4 Controller | T037 | Covered | + +## Summary Statistics -Validating admission webhook for constraints beyond CEL: Secret existence, ConfigMap references, cross-field provider ID validation. Deployed via kustomize. Cluster-scoped `ValidatingWebhookConfiguration` documented as an accepted deviation from constitution ยง1.1. - -### Expanded: Six New Edge Cases - -- CR update during active rollout (supersedes in-progress rollout) -- Operator upgrade with running instances (atomic image+config update) -- Config generation failure on update (preserve current Deployment) -- Deeply nested secretKeyRef (top-level only, deeper nesting passed through) -- Tools without toolRuntime provider (fallback to base config, then error) -- Shields without safety provider (same fallback pattern) - -### Refined: Existing Requirements - -- **FR-005**: secretKeyRef discovery depth constrained to top-level settings values only -- **FR-013**: overrideConfig ConfigMap must be in the same namespace as the CR -- **FR-020**: Expanded into FR-020/020a/020b/020c covering distribution resolution, status tracking, and image+config consistency -- **FR-032**: Env var naming clarified with provider ID example (`LLSD_VLLM_PRIMARY_API_KEY`) -- **FR-043/FR-044**: Tool/shield provider assignment now falls back to base config before erroring -- **FR-070**: Mutual exclusivity expanded to cover all four fields (`providers`, `resources`, `storage`, `disabled`) - -### New: Printer Columns (constitution ยง2.5) - -Default `kubectl get llsd`: Phase, Providers, Available, Age. Wide output adds Distribution image and Config name. - -### New: Status Fields - -- `resolvedDistribution` (image, configSource, configHash) for change detection across reconciliations and operator upgrades -- `DeploymentUpdated` and `Available` conditions added alongside existing `ConfigGenerated` and `SecretsResolved` - -### CRD Schema Corrections - -- API group fixed: `llamastack.io` (was `llamastack.ai` in draft) -- `targetCPUUtilizationPercentage` aligned with existing v1alpha1 naming -- Provider `host` field moved into `settings` (uniform provider schema) - ---- - -**Ready for detailed review?** See [spec.md](spec.md) for full requirements. +- **Total tasks**: 91 across 12 phases +- **Tasks per user story**: US1: 11, US2: 3, US3: 4, US4: 4, US5: 5, US6: 4, US7: 6, US8: 6 +- **Parallel execution streams**: 4 independent streams after foundational phase +- **MVP checkpoint**: Phase 3 (US1: minimal inference config) +- **Estimated implementation PRs**: 5 focused PRs diff --git a/specs/002-operator-generated-config/spec.md b/specs/002-operator-generated-config/spec.md index f70d5c74f..0797b3bdb 100644 --- a/specs/002-operator-generated-config/spec.md +++ b/specs/002-operator-generated-config/spec.md @@ -22,7 +22,7 @@ As a developer, I want to deploy a llama-stack instance with a vLLM backend usin **Acceptance Scenarios**: -1. **Given** a LLSD CR with `providers.inference: {provider: vllm, endpoint: "http://vllm:8000"}`, **When** I apply the CR, **Then** the operator generates a valid config.yaml with the vLLM provider configured +1. **Given** a LLSD CR with `providers.inference: [{provider: vllm, endpoint: "http://vllm:8000"}]`, **When** I apply the CR, **Then** the operator generates a valid config.yaml with the vLLM provider configured 2. **Given** a LLSD CR with `providers.inference.apiKey.secretKeyRef`, **When** I apply the CR, **Then** the secret value is injected via environment variable and the provider can authenticate 3. **Given** a LLSD CR with only `distribution.name: starter`, **When** I apply the CR, **Then** the distribution's default config.yaml is used unchanged @@ -36,9 +36,9 @@ As a platform engineer, I want to configure multiple inference providers (primar **Acceptance Scenarios**: -1. **Given** a LLSD CR with `providers.inference` as a list of two providers with explicit IDs, **When** I apply the CR, **Then** both providers are configured and accessible -2. **Given** a LLSD CR with multiple providers without explicit IDs, **When** I apply the CR, **Then** validation fails with a clear error message requiring explicit IDs -3. **Given** a LLSD CR with duplicate provider IDs, **When** I apply the CR, **Then** validation fails with a clear error message listing the duplicate IDs +1. **Given** a LLSD CR with `providers.inference` containing two `ProviderConfig` entries with explicit IDs, **When** I apply the CR, **Then** both providers are configured and accessible, and status condition `ConfigGenerated` is `True` +2. **Given** a LLSD CR with multiple providers in a list without explicit IDs, **When** I apply the CR, **Then** CEL validation rejects the CR at admission with a clear error message requiring explicit IDs +3. **Given** a LLSD CR with duplicate provider IDs across any provider types, **When** I apply the CR, **Then** CEL validation rejects the CR at admission with a clear error message listing the duplicate IDs ### User Story 3 - Resource Registration (Priority: P1) @@ -50,7 +50,7 @@ As a developer, I want to register models and tools declaratively in the CR, so **Acceptance Scenarios**: -1. **Given** a LLSD CR with `resources.models: ["llama3.2-8b"]`, **When** I apply the CR, **Then** the model is registered with the first configured inference provider +1. **Given** a LLSD CR with `resources.models: [{name: "llama3.2-8b"}]`, **When** I apply the CR, **Then** the model is registered with the first configured inference provider 2. **Given** a LLSD CR with a model specifying explicit provider assignment, **When** I apply the CR, **Then** the model is registered with the specified provider 3. **Given** a LLSD CR with `resources.tools: [websearch, rag]`, **When** I apply the CR, **Then** the tool groups are registered and available @@ -135,13 +135,13 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - What: `secretKeyRef` points to a secret that doesn't exist - Expected: Reconciliation fails with clear error, status shows "Secret not found: {name}" -- **Polymorphic expose with empty object**: +- **Expose with empty object**: - What: User specifies `expose: {}` - - Expected: Treated as `expose: true` (enabled with defaults) + - Expected: Treated as expose enabled with defaults (auto-generated hostname) - **Disabled APIs conflict with providers**: - What: User configures `providers.inference` but also `disabled: [inference]` - - Expected: Warning logged, disabled takes precedence, provider config is ignored + - Expected: Validation fails with error: "spec.providers.inference conflicts with spec.disabled: inference API is disabled but has providers configured. Remove the provider configuration or remove 'inference' from the disabled list." Accepting contradictory config and silently ignoring part of it leads to user confusion. Failing fast is more Kubernetes-idiomatic. - **Model references non-existent provider**: - What: `resources.models[].provider` references an ID not in `providers` @@ -163,9 +163,9 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - What: User changes CR in a way that produces an invalid merged config (e.g., references a provider type not supported by the distribution) - Expected: Operator keeps the current running config and Deployment unchanged, sets `ConfigGenerated=False` with a descriptive error, and does not trigger a Pod restart -- **Deeply nested secretKeyRef in settings**: - - What: User specifies `settings: {database: {connection: {secretKeyRef: {name: db, key: url}}}}` - - Expected: The nested secretKeyRef is NOT resolved as a secret reference. Only top-level settings values are inspected for secretKeyRef (e.g., `settings.host.secretKeyRef`). The deeply nested object is passed through to config.yaml as a literal map. +- **Secret references via settings vs secretRefs**: + - What: User puts a secretKeyRef inside `settings` instead of using the `secretRefs` field + - Expected: The `settings` map is passed through to config.yaml as-is without any secret resolution. Only the explicit `apiKey` and `secretRefs` fields trigger secret-to-env-var resolution. This is a clear, unambiguous boundary (no heuristic matching of map shapes). - **Tools specified without toolRuntime provider**: - What: User specifies `resources.tools: [websearch]` but does not configure `providers.toolRuntime` @@ -184,14 +184,14 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - **FR-001**: The CRD MUST define a new API version `v1alpha2` with the redesigned schema - **FR-002**: The `spec.distribution` field MUST support both `name` (mapped) and `image` (direct) forms, mutually exclusive - **FR-003**: The `spec.providers` section MUST support provider types: `inference`, `safety`, `vectorIo`, `toolRuntime`, `telemetry` -- **FR-004**: Each provider MUST support polymorphic form: single object OR list of objects with explicit `id` field -- **FR-005**: Each provider MUST support fields: `provider` (type), `endpoint`, `apiKey` (secretKeyRef), `settings` (escape hatch). Provider-specific connection fields (e.g., `host` for vectorIo) MUST use `secretKeyRef` within `settings` rather than top-level named fields, keeping the provider schema uniform. The operator MUST recognize `secretKeyRef` objects only at the top level of `settings` values (i.e., `settings..secretKeyRef`), not at arbitrary nesting depth. Deeper nesting is passed through to config.yaml as-is without secret resolution. +- **FR-004**: Each provider field MUST be a list of `ProviderConfig` objects (`[]ProviderConfig`). A single provider is expressed as a one-element list. This ensures kubebuilder validation markers apply to all provider fields and CEL rules can inspect provider IDs for uniqueness. +- **FR-005**: Each `ProviderConfig` MUST support fields: `id` (unique provider identifier), `provider` (type, required), `endpoint`, `apiKey` (secretKeyRef), `secretRefs` (named secret references map), `settings` (escape hatch). Provider-specific connection fields (e.g., `host` for vectorIo) MUST use `secretRefs` entries rather than embedding `secretKeyRef` inside `settings`. The `secretRefs` field is a `map[string]SecretKeyRef` where each key becomes the env var field suffix. The `settings` map is passed through to config.yaml as-is without secret resolution. - **FR-006**: The `spec.resources` section MUST support: `models`, `tools`, `shields` -- **FR-007**: Resources MUST support polymorphic form: simple string OR object with metadata +- **FR-007**: `resources.models` MUST be a list of `ModelConfig` objects (`[]ModelConfig`), where only the `name` field is required. Simple model references use `ModelConfig` with just `name` set. `resources.tools` and `resources.shields` MUST be lists of strings. - **FR-008**: The `spec.storage` section MUST have subsections: `kv` (key-value) and `sql` (relational) - **FR-009**: The `spec.disabled` field MUST be a list of API names to disable - **FR-010**: The `spec.networking` section MUST consolidate: `port`, `tls`, `expose`, `allowedFrom` -- **FR-011**: The `networking.expose` field MUST support polymorphic form: boolean OR object with `hostname` +- **FR-011**: The `networking.expose` field MUST be an object with optional `enabled` (bool) and `hostname` (string) fields. When `enabled` is true (or when the object is present with defaults), an Ingress/Route is created. When `hostname` is specified, it is used for the Ingress/Route hostname. - **FR-012**: The `spec.workload` section MUST contain K8s deployment settings: `replicas`, `workers`, `resources`, `autoscaling`, `storage`, `podDisruptionBudget`, `topologySpreadConstraints`, `overrides` - **FR-013**: The `spec.overrideConfig` field MUST be mutually exclusive with `providers`, `resources`, `storage`, `disabled`. The referenced ConfigMap MUST reside in the same namespace as the LLSD CR (consistent with namespace-scoped RBAC, constitution section 1.1) - **FR-014**: The `spec.externalProviders` field MUST remain for integration with spec 001 @@ -207,6 +207,7 @@ As a platform operator, I want to update the LLSD CR (e.g., add a provider, chan - **FR-023**: The operator MUST create a ConfigMap containing the generated config.yaml - **FR-024**: The ConfigMap name MUST include a content hash for change detection - **FR-025**: The operator MUST set owner references on the generated ConfigMap for garbage collection +- **FR-025a**: During each reconciliation, the controller MUST delete generated ConfigMaps beyond the most recent 2 (current + previous). ConfigMaps are identified by the `app.kubernetes.io/managed-by` label and sorted by creation timestamp. This prevents accumulation of stale ConfigMaps during the CR's lifetime. - **FR-026**: The operator MUST add a hash annotation to the Deployment to trigger rollouts on config changes - **FR-027**: The operator MUST detect the config.yaml schema version from the base configuration - **FR-028**: The operator MUST support config.yaml schema versions n and n-1 (current and previous) @@ -219,6 +220,7 @@ The base config extraction follows a phased approach. Phase 1 provides an implem **Phase 1 - Embedded Default Configs (MVP)** - **FR-027a**: The operator MUST include embedded default configurations for all distribution names defined in `distributions.json`, shipped as part of the operator binary +- **FR-027a1**: The base config extraction MUST be implemented behind a `ConfigResolver` interface with a `Resolve(image string) ([]byte, error)` method. Phase 1 provides `EmbeddedConfigResolver`; Phase 2 adds `OCIConfigResolver`. The controller selects the resolver based on available config sources (OCI labels take precedence over embedded, per FR-027g). - **FR-027b**: When `distribution.name` is specified, the operator MUST use the embedded config for that distribution as the base for config generation - **FR-027c**: When `distribution.image` is specified (direct image reference, no named distribution), the operator MUST require `overrideConfig.configMapName` to provide the base configuration. If `overrideConfig` is not set and no OCI config labels are found (see Phase 2), the operator MUST set `ConfigGenerated=False` with reason `BaseConfigRequired` and message: "Direct image references require either overrideConfig.configMapName or OCI config labels on the image. See docs/configuration.md for details." - **FR-027d**: The embedded configs MUST be versioned together with the distribution image mappings in `distributions.json`, ensuring each distribution name maps to a consistent (image, config) pair per operator release @@ -238,10 +240,10 @@ The base config extraction follows a phased approach. Phase 1 provides an implem - **FR-030**: Provider `provider` field MUST map to `provider_type` with `remote::` prefix (e.g., `vllm` becomes `remote::vllm`) - **FR-031**: Provider `endpoint` field MUST map to `config.url` in config.yaml -- **FR-032**: Provider `apiKey.secretKeyRef` MUST be resolved to an environment variable and referenced as `${env.LLSD__}`, where `` is the provider's unique `id` (explicit or auto-generated per FR-035), uppercased with hyphens replaced by underscores. Example: provider ID `vllm-primary` with field `apiKey` produces `LLSD_VLLM_PRIMARY_API_KEY`. +- **FR-032**: Provider `apiKey.secretKeyRef` and `secretRefs` entries MUST be resolved to environment variables and referenced as `${env.LLSD__}`, where `` is the provider's unique `id` (explicit or auto-generated per FR-035), uppercased with hyphens replaced by underscores. For `apiKey`, the field suffix is `API_KEY`. For `secretRefs`, the map key is uppercased with hyphens replaced by underscores. Example: provider ID `vllm-primary` with `apiKey` produces `LLSD_VLLM_PRIMARY_API_KEY`; `secretRefs.host` produces `LLSD_VLLM_PRIMARY_HOST`. - **FR-033**: Provider `settings` MUST be merged into the provider's `config` section in config.yaml -- **FR-034**: When multiple providers are specified, each MUST have an explicit `id` field -- **FR-035**: Single provider without `id` MUST auto-generate `provider_id` from the `provider` field value +- **FR-034**: When multiple providers are specified for the same API type, each MUST have an explicit `id` field. CEL validation enforces this at admission time. +- **FR-035**: A single provider (one-element list) without `id` MUST auto-generate `provider_id` from the `provider` field value #### Telemetry Provider @@ -278,14 +280,19 @@ The base config extraction follows a phased approach. Phase 1 provides an implem #### Validation - **FR-070**: CEL validation MUST enforce mutual exclusivity between `overrideConfig` and each of `providers`, `resources`, `storage`, and `disabled` -- **FR-071**: CEL validation MUST require explicit `id` when multiple providers are specified for the same API -- **FR-072**: CEL validation MUST enforce unique provider IDs across all provider types +- **FR-071**: CEL validation MUST require explicit `id` on each `ProviderConfig` when a provider list has more than one element. Rule applied per provider type field (e.g., `self.providers.inference.size() <= 1 || self.providers.inference.all(p, has(p.id))`) +- **FR-072**: CEL validation MUST enforce unique provider IDs across all provider types. Since providers are typed slices, CEL can inspect IDs directly (e.g., check that the union of all provider IDs has no duplicates) - **FR-073**: Controller validation MUST verify referenced Secrets exist before generating config - **FR-074**: Controller validation MUST verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` - **FR-075**: Validation errors MUST include actionable messages with field paths - **FR-076**: A validating admission webhook MUST validate CR creation and update operations for constraints that cannot be expressed in CEL (e.g., Secret existence checks, ConfigMap existence for `overrideConfig`, cross-field semantic validation such as provider ID references in resources) - **FR-077**: The validating webhook MUST return structured error responses with field paths and actionable messages following Kubernetes API conventions - **FR-078**: The validating webhook MUST be deployed as part of the operator installation and configured via the operator's kustomize manifests with appropriate certificate management +- **FR-079**: CEL validation MUST enforce that `networking.tls.secretName` is required when `networking.tls.enabled` is true: `!self.tls.enabled || has(self.tls.secretName)` +- **FR-079a**: CEL validation MUST enforce that `storage.kv.endpoint` is required when `storage.kv.type` is `redis`: `self.storage.kv.type != 'redis' || has(self.storage.kv.endpoint)` +- **FR-079b**: CEL validation MUST enforce that `storage.sql.connectionString` is required when `storage.sql.type` is `postgres`: `self.storage.sql.type != 'postgres' || has(self.storage.sql.connectionString)` +- **FR-079c**: CEL validation SHOULD warn when `storage.kv.endpoint` or `storage.kv.password` are specified with `storage.kv.type: sqlite` (unused fields) +- **FR-079d**: The validating webhook MUST reject CRs where `distribution.name` references a name not present in the embedded distribution registry (`distributions.json`). The error message MUST list available distribution names: "Unknown distribution \"{name}\". Available distributions: {list}" #### API Version Conversion @@ -321,6 +328,7 @@ The base config extraction follows a phased approach. Phase 1 provides an implem - **NFR-004**: Error messages MUST be actionable (user can resolve without operator knowledge) - **NFR-005**: The generated ConfigMap MUST be immutable (new ConfigMap on changes, not updates) - **NFR-006**: Config extraction from images MUST be cached to avoid repeated image pulls +- **NFR-007**: The operator MUST emit Kubernetes Events for significant state changes: `ConfigGenerated` (Normal), `DeploymentUpdated` (Normal), `ConfigGenerationFailed` (Warning), `SecretResolutionFailed` (Warning). Events provide a time-ordered audit trail complementing status conditions (which only show current state). ### External Dependencies @@ -369,7 +377,7 @@ crane mutate ${IMAGE}:build \ - **StorageSpec**: Configuration for state storage (kv and sql backends) - **NetworkingSpec**: Configuration for network exposure (port, TLS, expose, allowedFrom) - **WorkloadSpec**: Kubernetes deployment settings (replicas, resources, autoscaling) -- **ExposeConfig**: Polymorphic expose configuration (bool or object with hostname) +- **ExposeConfig**: Expose configuration with `enabled` (bool) and `hostname` (string) fields - **ResolvedDistributionStatus**: Tracks the resolved image reference, config source (embedded/oci-label), and config hash for change detection ## CRD Schema @@ -395,16 +403,16 @@ spec: settings: max_tokens: 8192 safety: - provider: llama-guard + - provider: llama-guard vectorIo: - provider: pgvector - settings: - host: - secretKeyRef: {name: pg-creds, key: host} + - provider: pgvector + secretRefs: + host: + secretKeyRef: {name: pg-creds, key: host} resources: models: - - "llama3.2-8b" + - name: "llama3.2-8b" - name: "llama3.2-70b" provider: vllm-primary contextLength: 128000 @@ -433,8 +441,9 @@ spec: secretName: llama-tls caBundle: configMapName: custom-ca - expose: true - # OR: expose: {hostname: "llama.example.com"} + expose: + enabled: true + # hostname: "llama.example.com" # Optional: custom hostname allowedFrom: namespaces: ["app-ns"] labels: ["llama-access"] @@ -576,8 +585,8 @@ spec: | Tier | Use Case | Mechanism | Example | |------|----------|-----------|---------| -| 1 | Simple (80%) | Inline provider fields | `providers.inference: {provider: vllm, endpoint: "..."}` | -| 2 | Advanced (15%) | Per-provider settings | `providers.inference: {..., settings: {max_tokens: 8192}}` | +| 1 | Simple (80%) | Inline provider fields | `providers.inference: [{provider: vllm, endpoint: "..."}]` | +| 2 | Advanced (15%) | Per-provider settings | `providers.inference: [{..., settings: {max_tokens: 8192}}]` | | 3 | Full Control (5%) | ConfigMap override | `overrideConfig: {configMapName: my-config}` | ## Status Reporting @@ -645,12 +654,22 @@ status: - **Environment Variable Naming**: Use deterministic, prefixed names: `LLSD__` (e.g., `LLSD_VLLM_PRIMARY_API_KEY`). Provider ID is uppercased with hyphens replaced by underscores. - **ConfigMap Permissions**: Generated ConfigMaps inherit namespace RBAC - **Image Extraction**: Config extraction from images uses read-only operations -- **Webhook Permissions**: The `ValidatingWebhookConfiguration` is a cluster-scoped resource, installed by OLM or kustomize during operator setup (not by the operator at runtime). This is a standard pattern for Kubernetes operators with admission webhooks and is an accepted deviation from constitution ยง1.1. The operator itself remains namespace-scoped at runtime. +- **Webhook Permissions**: The `ValidatingWebhookConfiguration` is a cluster-scoped resource, installed by OLM or kustomize during operator setup (not by the operator at runtime). This is a standard pattern for Kubernetes operators with admission webhooks and is an accepted deviation from constitution ยง1.1 (documented per constitution ยงExceptions). The operator itself remains namespace-scoped at runtime. +- **Disabled + Provider Conflict**: Specifying providers for a disabled API type is a validation error, not a warning. This prevents contradictory config from being accepted and silently ignored. + +## Clarifications + +### Session 2026-03-10 + +- Q: How should the operator clean up old generated ConfigMaps during a CR's lifetime? โ†’ A: Controller deletes ConfigMaps beyond the last 2 during each reconciliation. +- Q: How should Phase 1 code handle the Phase 2 (OCI label) extension point? โ†’ A: Define a `ConfigResolver` interface now, implement `EmbeddedConfigResolver` for Phase 1. Phase 2 adds an `OCIConfigResolver` without refactoring. +- Q: Should the operator emit Kubernetes Events for config generation lifecycle? โ†’ A: Yes, emit Events for: config generated, deployment updated, config generation failed, secret resolution failed. +- Q: When should unknown distribution name be validated? โ†’ A: Webhook validates at admission time for instant feedback. The distribution registry is a static embedded resource, cheap to load in the webhook. ## Open Questions - ~~**OQ-001**~~: Resolved. `expose: {}` is treated as `expose: true` (see Edge Cases). -- ~~**OQ-002**~~: Resolved. Disabled API + provider config conflict produces a **warning** (not an error). The `disabled` list takes precedence: the provider config is accepted but ignored at runtime. Warning is logged and reported in status conditions. (From PR #242 review; resolved per edge case "Disabled APIs conflict with providers") +- ~~**OQ-002**~~: Resolved. Disabled API + provider config conflict produces a **validation error** (not a warning). Accepting contradictory config and silently ignoring part of it leads to user confusion. The webhook rejects CRs where a provider type appears in both `providers` and `disabled`. (Originally resolved as warning in PR #242 review, changed to error after PR #253 review feedback from eoinfennessy) - ~~**OQ-003**~~: Resolved. Environment variable naming uses the **provider ID** (not provider type or API type): `LLSD__`. The provider ID is unique across all providers (enforced by FR-072), ensuring no collisions. For single providers without explicit `id`, the auto-generated ID from FR-035 is used. Examples: `LLSD_VLLM_PRIMARY_API_KEY`, `LLSD_PGVECTOR_HOST`. Characters not valid in env var names (hyphens) are replaced with underscores and uppercased. (From PR #242 review) - **OQ-004**: Should the operator create a default LlamaStackDistribution instance when installed? This is uncommon for Kubernetes operators but could improve the getting-started experience. If adopted, it should be opt-in via operator configuration (e.g., a Helm value or OLM parameter). (From team discussion, 2026-02-10) diff --git a/specs/002-operator-generated-config/tasks.md b/specs/002-operator-generated-config/tasks.md index 873e06a13..8739e8a4e 100644 --- a/specs/002-operator-generated-config/tasks.md +++ b/specs/002-operator-generated-config/tasks.md @@ -1,1123 +1,342 @@ -# Implementation Tasks: Operator-Generated Server Configuration (v1alpha2) +# Tasks: Operator-Generated Server Configuration (v1alpha2) -**Spec**: 002-operator-generated-config -**Created**: 2026-02-02 +**Input**: Design documents from `/specs/002-operator-generated-config/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ -## Task Overview +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. -| Phase | Tasks | Priority | -|-------|-------|----------| -| Phase 1: CRD Schema | 8 tasks | P1 | -| Phase 2: Config Generation | 8 tasks | P1 | -| Phase 3: Controller Integration | 12 tasks | P1 | -| Phase 4: Conversion Webhook | 4 tasks | P2 | -| Phase 5: Testing & Docs | 6 tasks | P2 | +## Format: `[ID] [P?] [Story] Description` ---- - -## Phase 1: CRD Schema (v1alpha2) - -### Task 1.1: Create v1alpha2 API Directory Structure - -**Priority**: P1 -**Blocked by**: None - -**Description**: -Create the v1alpha2 API package with groupversion_info.go and base types. - -**Files to create**: -- `api/v1alpha2/groupversion_info.go` -- `api/v1alpha2/doc.go` - -**Acceptance criteria**: -- [ ] v1alpha2 package compiles -- [ ] GroupVersion is `llamastack.io/v1alpha2` -- [ ] Scheme registration works - ---- - -### Task 1.2: Define Provider Types - -**Priority**: P1 -**Blocked by**: 1.1 - -**Description**: -Define ProvidersSpec and ProviderConfig types with polymorphic support. - -**Types to define**: -- `ProvidersSpec` (inference, safety, vectorIo, toolRuntime, telemetry) -- `ProviderConfig` (id, provider, endpoint, apiKey, settings) -- `ProviderConfigOrList` (polymorphic wrapper using json.RawMessage) -- `SecretKeyRef` (name, key) - -**Requirements covered**: FR-003, FR-004, FR-005 - -**Acceptance criteria**: -- [ ] Types marshal/unmarshal correctly -- [ ] Polymorphic parsing works (single and list forms) -- [ ] Kubebuilder validation tags present - ---- - -### Task 1.3: Define Resource Types - -**Priority**: P1 -**Blocked by**: 1.1 - -**Description**: -Define ResourcesSpec and model/tool types with polymorphic support. - -**Types to define**: -- `ResourcesSpec` (models, tools, shields) -- `ModelConfig` (name, provider, contextLength, modelType, quantization) -- `ModelConfigOrString` (polymorphic wrapper) - -**Requirements covered**: FR-006, FR-007 - -**Acceptance criteria**: -- [ ] Types marshal/unmarshal correctly -- [ ] Simple string and object forms both work -- [ ] Kubebuilder validation tags present - ---- - -### Task 1.4: Define Storage Types - -**Priority**: P1 -**Blocked by**: 1.1 - -**Description**: -Define StorageSpec with kv and sql subsections. - -**Types to define**: -- `StorageSpec` (kv, sql) -- `KVStorageSpec` (type, endpoint, password) -- `SQLStorageSpec` (type, connectionString) - -**Requirements covered**: FR-008, FR-050, FR-050a, FR-051, FR-052 - -**Acceptance criteria**: -- [ ] Types marshal/unmarshal correctly -- [ ] SecretKeyRef references work -- [ ] Enum validation for type field - ---- - -### Task 1.5: Define Networking Types - -**Priority**: P1 -**Blocked by**: 1.1 - -**Description**: -Define NetworkingSpec with polymorphic expose support. - -**Types to define**: -- `NetworkingSpec` (port, tls, expose, allowedFrom) -- `TLSSpec` (enabled, secretName, caBundle) -- `ExposeConfig` (enabled, hostname - polymorphic) -- `CABundleConfig` (from v1alpha1) - -**Requirements covered**: FR-010, FR-011 - -**Acceptance criteria**: -- [ ] Types marshal/unmarshal correctly -- [ ] Polymorphic expose handles bool and object forms -- [ ] Defaults applied (port: 8321) - ---- - -### Task 1.6: Define Workload Types - -**Priority**: P1 -**Blocked by**: 1.1 - -**Description**: -Define WorkloadSpec consolidating K8s deployment settings. - -**Types to define**: -- `WorkloadSpec` (replicas, workers, resources, autoscaling, storage, pdb, topologySpread, overrides) -- `WorkloadOverrides` (serviceAccountName, env, command, args, volumes, volumeMounts) -- `PVCStorageSpec` (size, mountPath) -- Reuse existing: `AutoscalingSpec`, `PodDisruptionBudgetSpec` - -**Requirements covered**: FR-012 - -**Acceptance criteria**: -- [ ] Types marshal/unmarshal correctly -- [ ] All fields from v1alpha1 ServerSpec accounted for -- [ ] Kubebuilder validation tags present - ---- - -### Task 1.7: Define Main Spec and Add CEL Validation - -**Priority**: P1 -**Blocked by**: 1.2, 1.3, 1.4, 1.5, 1.6 - -**Description**: -Create the main LlamaStackDistributionSpec with all sections and add CEL validation rules. - -**Types to define**: -- `LlamaStackDistributionSpec` (distribution, providers, resources, storage, disabled, networking, workload, externalProviders, overrideConfig) -- `LlamaStackDistribution` (main CRD type) -- `LlamaStackDistributionList` -- `OverrideConfigSpec` (configMapName; must be in same namespace as CR) - -**CEL validations**: -- Mutual exclusivity: providers vs overrideConfig -- Mutual exclusivity: resources vs overrideConfig -- Mutual exclusivity: storage vs overrideConfig -- Mutual exclusivity: disabled vs overrideConfig - -**Requirements covered**: FR-001, FR-002, FR-009, FR-013, FR-014, FR-070 - -**Printer columns** (constitution ยง2.5): -- `Phase` (`.status.phase`) -- `Distribution` (`.status.resolvedDistribution.image`, priority=1, wide output only) -- `Config` (`.status.configGeneration.configMapName`, priority=1, wide output only) -- `Providers` (`.status.configGeneration.providerCount`) -- `Available` (`.status.availableReplicas`) -- `Age` (`.metadata.creationTimestamp`) - -**Acceptance criteria**: -- [ ] Complete spec structure compiles -- [ ] CEL validations reject invalid combinations -- [ ] Printer columns defined: Phase, Providers, Available, Age (default); Distribution, Config (wide) - ---- - -### Task 1.8: Generate CRD Manifests and Verify - -**Priority**: P1 -**Blocked by**: 1.7 - -**Description**: -Run code generation and verify CRD manifests are correct. - -**Commands**: -```bash -make generate -make manifests -``` - -**Verification**: -- [ ] CRD YAML generated in `config/crd/bases/` -- [ ] OpenAPI schema includes all new fields -- [ ] CEL validation rules in CRD -- [ ] Both v1alpha1 and v1alpha2 versions present -- [ ] v1alpha2 is storage version - ---- - -## Phase 2: Config Generation Engine - -### Task 2.1: Create Config Package Structure - -**Priority**: P1 -**Blocked by**: 1.7 - -**Description**: -Create the pkg/config package directory structure with basic types. - -**Files to create**: -- `pkg/config/types.go` - Internal config types -- `pkg/config/config.go` - Main orchestration (stub) - -**Acceptance criteria**: -- [ ] Package compiles -- [ ] Internal types defined for config.yaml structure - ---- - -### Task 2.2: Implement Base Config Resolution (Phased) - -**Priority**: P1 -**Blocked by**: 2.1 - -**Description**: -Implement base config resolution with a phased approach. Phase 1 (MVP) uses configs embedded in the operator binary via `go:embed`. Phase 2 (Enhancement) adds OCI label-based extraction as an optional override. - -**Files**: -- `pkg/config/resolver.go` - BaseConfigResolver with resolution priority logic -- `configs/` - Embedded default config directory (one `config.yaml` per named distribution) -- `Makefile` - Build-time validation target (`validate-configs`) - -**Phase 1 (MVP) Approach**: -1. Create `configs//config.yaml` for each distribution in `distributions.json` -2. Embed via `//go:embed configs` in the resolver package -3. On resolution: lookup embedded config by `distribution.name` -4. For `distribution.image` without OCI labels: require `overrideConfig` - -**Phase 2 (Enhancement) Approach**: -1. Add `pkg/config/oci_extractor.go` using `k8schain` for registry auth -2. Check OCI labels on resolved image first (takes precedence over embedded) -3. Fall back to embedded config if no labels found -4. Cache by image digest - -**Functions**: -- `NewBaseConfigResolver(distributionImages, imageOverrides) *BaseConfigResolver` -- `(r *BaseConfigResolver) Resolve(ctx, distribution) (*BaseConfig, string, error)` -- `(r *BaseConfigResolver) loadEmbeddedConfig(name) (*BaseConfig, error)` -- `(r *BaseConfigResolver) resolveImage(distribution) (string, error)` - -**Requirements covered**: FR-020, FR-027a through FR-027e (Phase 1), FR-027f through FR-027j (Phase 2), NFR-006 - -**Acceptance criteria**: -- [ ] Embedded configs loaded via `go:embed` for all named distributions -- [ ] `distribution.name` resolves to embedded config -- [ ] `distribution.image` without OCI labels returns clear error requiring `overrideConfig` -- [ ] Build-time validation ensures all distributions have configs -- [ ] (Phase 2) OCI label extraction takes precedence over embedded when available -- [ ] (Phase 2) Caching by image digest prevents repeated extraction -- [ ] Unit tests for resolution priority logic - ---- - -### Task 2.3: Implement Config Version Detection - -**Priority**: P1 -**Blocked by**: 2.2 - -**Description**: -Implement config.yaml schema version detection and validation. - -**File**: `pkg/config/version.go` - -**Functions**: -- `DetectConfigVersion(config) (int, error)` -- `ValidateConfigVersion(version) error` -- `SupportedVersions() []int` - -**Requirements covered**: FR-027, FR-028, FR-029 - -**Acceptance criteria**: -- [ ] Detects version from base config -- [ ] Validates against supported versions (n, n-1) -- [ ] Returns clear error for unsupported versions - ---- - -### Task 2.4: Implement Provider Expansion - -**Priority**: P1 -**Blocked by**: 2.1 - -**Description**: -Implement provider spec expansion to config.yaml format. - -**File**: `pkg/config/provider.go` - -**Functions**: -- `ExpandProviders(spec) ([]ProviderConfig, error)` -- `NormalizeProviderType(provider) string` -- `GenerateProviderID(providerType) string` -- `ParsePolymorphicProvider(raw json.RawMessage) ([]ProviderConfig, error)` - -**Requirements covered**: FR-030, FR-031, FR-033, FR-034, FR-035 - -**Acceptance criteria**: -- [ ] Single provider expands correctly -- [ ] List of providers expands correctly -- [ ] Auto-generates IDs for single providers -- [ ] Merges settings into config section - ---- - -### Task 2.5: Implement Resource Expansion - -**Priority**: P1 -**Blocked by**: 2.4 - -**Description**: -Implement resource spec expansion to registered_resources format. - -**File**: `pkg/config/resource.go` - -**Functions**: -- `ExpandResources(spec, providers) (*RegisteredResources, error)` -- `GetDefaultInferenceProvider(providers) string` -- `ParsePolymorphicModel(raw) ([]ModelConfig, error)` - -**Requirements covered**: FR-040, FR-041, FR-042, FR-043, FR-044 - -**Acceptance criteria**: -- [ ] Simple model strings expand correctly -- [ ] Model objects expand correctly -- [ ] Default provider assignment works -- [ ] Tools and shields expand correctly -- [ ] Tools fail with actionable error when no toolRuntime provider exists (user or base config) -- [ ] Shields fail with actionable error when no safety provider exists (user or base config) - ---- - -### Task 2.6: Implement Storage Expansion - -**Priority**: P1 -**Blocked by**: 2.1 - -**Description**: -Implement storage spec expansion to config.yaml format. +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions -**File**: `pkg/config/storage.go` +## Phase 1: Setup -**Functions**: -- `ExpandStorage(spec, base) (*StorageConfig, error)` -- `ExpandKVStorage(kv) (*KVConfig, error)` -- `ExpandSQLStorage(sql) (*SQLConfig, error)` +**Purpose**: Project scaffolding for v1alpha2 API version -**Requirements covered**: FR-050, FR-050a, FR-051, FR-052, FR-053 - -**Acceptance criteria**: -- [ ] KV storage (sqlite, redis) expands correctly -- [ ] SQL storage (sqlite, postgres) expands correctly -- [ ] Preserves defaults when not specified +- [ ] T001 (llama-stack-k8s-operator-6nd.1) Create v1alpha2 package scaffolding in api/v1alpha2/doc.go and api/v1alpha2/groupversion_info.go +- [ ] T002 (llama-stack-k8s-operator-6nd.2) Define v1alpha2 LlamaStackDistribution root type with kubebuilder markers in api/v1alpha2/llamastackdistribution_types.go +- [ ] T003 (llama-stack-k8s-operator-6nd.3) [P] Define DistributionSpec type (name/image mutually exclusive via CEL) in api/v1alpha2/llamastackdistribution_types.go +- [ ] T004 (llama-stack-k8s-operator-6nd.4) [P] Define ProvidersSpec and ProviderConfig types with typed []ProviderConfig slices in api/v1alpha2/llamastackdistribution_types.go +- [ ] T005 (llama-stack-k8s-operator-6nd.5) [P] Define SecretKeyRef type in api/v1alpha2/llamastackdistribution_types.go +- [ ] T006 (llama-stack-k8s-operator-6nd.6) [P] Define ResourcesSpec and ModelConfig types (name required, provider optional) in api/v1alpha2/llamastackdistribution_types.go +- [ ] T007 (llama-stack-k8s-operator-6nd.7) [P] Define StorageSpec, KVStorageSpec, SQLStorageSpec types with enum validation in api/v1alpha2/llamastackdistribution_types.go +- [ ] T008 (llama-stack-k8s-operator-6nd.8) [P] Define NetworkingSpec, TLSSpec, ExposeConfig, CABundleConfig types in api/v1alpha2/llamastackdistribution_types.go +- [ ] T009 (llama-stack-k8s-operator-6nd.9) [P] Define WorkloadSpec, WorkloadOverrides, AutoscalingSpec, PodDisruptionBudgetSpec types in api/v1alpha2/llamastackdistribution_types.go +- [ ] T010 (llama-stack-k8s-operator-6nd.10) [P] Define OverrideConfigSpec type in api/v1alpha2/llamastackdistribution_types.go +- [ ] T011 (llama-stack-k8s-operator-6nd.11) Define v1alpha2 status types (ResolvedDistributionStatus, ConfigGenerationStatus, condition constants) in api/v1alpha2/llamastackdistribution_types.go +- [ ] T012 (llama-stack-k8s-operator-6nd.12) Add CEL validation rules to LlamaStackDistributionSpec: mutual exclusivity, provider ID uniqueness, TLS/storage conditional fields, disabled+provider conflict in api/v1alpha2/llamastackdistribution_types.go +- [ ] T013 (llama-stack-k8s-operator-6nd.13) Run controller-gen to generate deepcopy and CRD manifests, verify CEL rules compile in config/crd/bases/llamastack.io_llamastackdistributions.yaml --- -### Task 2.7: Implement Secret Resolution - -**Priority**: P1 -**Blocked by**: 2.4, 2.6 - -**Description**: -Implement secret reference resolution to environment variables. - -**File**: `pkg/config/secret_resolver.go` +## Phase 2: Foundational (Config Generation Engine) -**Functions**: -- `ResolveSecrets(spec) (*SecretResolution, error)` -- `GenerateEnvVarName(providerType, field) string` -- `CollectSecretRefs(spec) []SecretRef` +**Purpose**: Core config generation pipeline that all user stories depend on -**Types**: -```go -type SecretResolution struct { - EnvVars []corev1.EnvVar - Substitutions map[string]string // placeholder -> ${env.VAR} -} -``` - -**Requirements covered**: FR-022, FR-032, NFR-003 - -**Acceptance criteria**: -- [ ] Collects all secretKeyRef references -- [ ] Generates deterministic env var names -- [ ] Creates env var definitions for Deployment -- [ ] Returns substitution map for config generation - ---- - -### Task 2.8: Implement Config Generation Orchestration - -**Priority**: P1 -**Blocked by**: 2.2, 2.3, 2.4, 2.5, 2.6, 2.7 - -**Description**: -Implement the main config generation orchestration. - -**File**: `pkg/config/config.go` - -**Functions**: -- `GenerateConfig(ctx, spec, image) (*GeneratedConfig, error)` -- `MergeConfig(base, user) (map[string]interface{}, error)` -- `ApplyDisabledAPIs(config, disabled) map[string]interface{}` -- `RenderConfigYAML(config) (string, error)` -- `ComputeContentHash(yaml string) string` - -**Types**: -```go -type GeneratedConfig struct { - ConfigYAML string - EnvVars []corev1.EnvVar - ContentHash string - ProviderCount int - ResourceCount int - ConfigVersion int -} -``` +**CRITICAL**: No user story work can begin until this phase is complete -**Requirements covered**: FR-021, FR-023, FR-024, NFR-001 +- [ ] T014 (llama-stack-k8s-operator-834.1) Define ConfigResolver interface and implement EmbeddedConfigResolver with go:embed in pkg/config/resolver.go +- [ ] T015 (llama-stack-k8s-operator-834.2) [P] Create embedded default config files for starter distribution in pkg/deploy/configs/starter/config.yaml +- [ ] T016 (llama-stack-k8s-operator-834.3) [P] Create embedded default config files for remote-vllm distribution in pkg/deploy/configs/remote-vllm/config.yaml +- [ ] T017 (llama-stack-k8s-operator-834.4) [P] Implement config version detection in pkg/config/version.go +- [ ] T018 (llama-stack-k8s-operator-834.5) Implement base config parsing and merge logic (full API-type replacement for providers, per-subsection for storage, additive for resources, subtractive for disabled) in pkg/config/merge.go +- [ ] T019 (llama-stack-k8s-operator-834.6) [P] Implement provider expansion (auto-ID generation, remote:: prefix, endpoint mapping, settings merge) in pkg/config/provider.go +- [ ] T020 (llama-stack-k8s-operator-834.7) [P] Implement secret reference collection from apiKey and secretRefs fields, env var name generation (LLSD__) in pkg/config/secret.go +- [ ] T021 (llama-stack-k8s-operator-834.8) [P] Implement resource expansion (model registration with provider assignment, tool group registration, shield registration) in pkg/config/resource.go +- [ ] T022 (llama-stack-k8s-operator-834.9) [P] Implement storage config generation (kv and sql backend mapping) in pkg/config/storage.go +- [ ] T023 (llama-stack-k8s-operator-834.10) Implement GenerateConfig orchestrator that calls all expansion functions and produces GeneratedConfig (configYAML, contentHash, envVars, counts) in pkg/config/generator.go +- [ ] T024 (llama-stack-k8s-operator-834.11) [P] Write unit tests for EmbeddedConfigResolver in pkg/config/resolver_test.go +- [ ] T025 (llama-stack-k8s-operator-834.12) [P] Write unit tests for provider expansion (single provider, multiple providers, auto-ID, settings merge, secretRefs) in pkg/config/provider_test.go +- [ ] T026 (llama-stack-k8s-operator-834.13) [P] Write unit tests for resource expansion (model with/without provider, tools, shields, missing provider error) in pkg/config/resource_test.go +- [ ] T027 (llama-stack-k8s-operator-834.14) [P] Write unit tests for secret collection (apiKey, secretRefs, storage secrets, env var naming normalization) in pkg/config/secret_test.go +- [ ] T028 (llama-stack-k8s-operator-834.15) [P] Write unit tests for storage config (sqlite defaults, redis with endpoint, postgres with connectionString) in pkg/config/storage_test.go +- [ ] T029 (llama-stack-k8s-operator-834.16) [P] Write unit tests for merge logic (provider replacement, storage merge, resource additive, disabled subtractive) in pkg/config/merge_test.go +- [ ] T030 (llama-stack-k8s-operator-834.17) Write integration tests for GenerateConfig end-to-end with golden file comparison for determinism in pkg/config/generator_test.go -**Acceptance criteria**: -- [ ] Generates complete config.yaml -- [ ] Merges user config over base -- [ ] Applies disabled APIs -- [ ] Returns content hash -- [ ] Deterministic output (same input โ†’ same output) +**Checkpoint**: Config generation pipeline is complete and independently testable via unit tests --- -## Phase 3: Controller Integration +## Phase 3: User Story 1 - Simple Inference Configuration (Priority: P1) MVP -### Task 3.1: Extend Controller for v1alpha2 +**Goal**: Deploy a llama-stack instance with a vLLM backend using minimal YAML, operator generates config.yaml -**Priority**: P1 -**Blocked by**: Phase 1, Phase 2 +**Independent Test**: Deploy a LLSD CR with minimal `providers.inference` configuration and verify the server starts with the provider accessible via the `/v1/providers` API -**Description**: -Extend the existing controller to handle v1alpha2 resources. +### Implementation for User Story 1 -**File**: `controllers/llamastackdistribution_controller.go` +- [ ] T031 (llama-stack-k8s-operator-db3.1) [US1] Implement ConfigMap reconciler: create immutable ConfigMap with content-hash name, set owner reference, compare hash for no-op detection in controllers/configmap_reconciler.go +- [ ] T032 (llama-stack-k8s-operator-db3.2) [US1] Implement ConfigMap cleanup: delete generated ConfigMaps beyond the last 2 during reconciliation in controllers/configmap_reconciler.go +- [ ] T033 (llama-stack-k8s-operator-db3.3) [US1] Implement v1alpha2 controller helpers: determine config path (overrideConfig vs generate vs default), resolve distribution name to image in controllers/v1alpha2_helpers.go +- [ ] T034 (llama-stack-k8s-operator-db3.4) [US1] Modify reconcile loop to support v1alpha2 CRs: add config generation path between distribution resolution and Deployment creation in controllers/llamastackdistribution_controller.go +- [ ] T035 (llama-stack-k8s-operator-db3.5) [US1] Implement atomic Deployment update: apply image, ConfigMap volume, env vars, and hash annotation in a single client.Update() call in controllers/llamastackdistribution_controller.go +- [ ] T036 (llama-stack-k8s-operator-db3.6) [US1] Wire ConfigGenerated status condition (True on success, False on failure with reason) in controllers/status.go +- [ ] T037 (llama-stack-k8s-operator-db3.7) [US1] Emit Kubernetes Events for config generation success and failure (NFR-007) in controllers/llamastackdistribution_controller.go +- [ ] T038 (llama-stack-k8s-operator-db3.8) [US1] Write unit tests for ConfigMap reconciler (create, hash comparison, cleanup) in controllers/configmap_reconciler_test.go +- [ ] T039 (llama-stack-k8s-operator-db3.9) [US1] Write unit tests for v1alpha2 helpers (config path routing, distribution resolution) in controllers/v1alpha2_helpers_test.go +- [ ] T040 (llama-stack-k8s-operator-db3.10) [US1] Write controller test: minimal v1alpha2 CR with single inference provider generates ConfigMap and creates Deployment in controllers/llamastackdistribution_controller_test.go +- [ ] T041 (llama-stack-k8s-operator-db3.11) [US1] Create v1alpha2 sample CR: minimal inference setup in config/samples/v1alpha2/minimal-inference.yaml -**Changes**: -- Add v1alpha2 type imports -- Update Reconcile() to handle both versions -- Add helper functions for version detection - -**Acceptance criteria**: -- [ ] Controller handles v1alpha2 resources -- [ ] Existing v1alpha1 behavior unchanged -- [ ] Version-specific logic isolated +**Checkpoint**: User Story 1 complete. Minimal v1alpha2 CR with one provider generates config and deploys. --- -### Task 3.2: Implement Config Source Determination - -**Priority**: P1 -**Blocked by**: 3.1 +## Phase 4: User Story 2 - Multiple Providers Configuration (Priority: P1) -**Description**: -Implement logic to determine config source (generated, override, or default). +**Goal**: Configure multiple inference providers (primary and fallback) in a single LLSD with explicit IDs -**Function**: `DetermineConfigSource(instance) ConfigSource` +**Independent Test**: Deploy a LLSD CR with multiple inference providers using list form, verify all providers appear in the `/v1/providers` API -**Logic**: -- If overrideConfig specified โ†’ ConfigSourceOverride -- If providers/resources/storage specified โ†’ ConfigSourceGenerated -- Otherwise โ†’ ConfigSourceDistributionDefault +### Implementation for User Story 2 -**Requirements covered**: FR-013 +- [ ] T042 (llama-stack-k8s-operator-een.1) [US2] Write controller test: v1alpha2 CR with two inference providers with explicit IDs, verify both appear in generated config in controllers/llamastackdistribution_controller_test.go +- [ ] T043 (llama-stack-k8s-operator-een.2) [US2] Write controller test: v1alpha2 CR with duplicate provider IDs rejected by CEL validation in controllers/llamastackdistribution_controller_test.go +- [ ] T044 (llama-stack-k8s-operator-een.3) [US2] Create v1alpha2 sample CR: multiple providers with explicit IDs in config/samples/v1alpha2/multiple-providers.yaml -**Acceptance criteria**: -- [ ] Correctly identifies config source -- [ ] Handles all combinations +**Checkpoint**: User Story 2 complete. Multiple providers per API type work with ID validation. --- -### Task 3.3: Implement Generated ConfigMap Reconciliation +## Phase 5: User Story 3 - Resource Registration (Priority: P1) -**Priority**: P1 -**Blocked by**: 3.2 +**Goal**: Register models and tools declaratively in the CR, available immediately on server start -**Description**: -Implement creation and management of generated ConfigMaps. +**Independent Test**: Deploy a LLSD CR with `resources.models` and `resources.tools`, verify resources appear in the respective API endpoints -**Function**: `ReconcileGeneratedConfigMap(ctx, instance) error` +### Implementation for User Story 3 -**Logic**: -1. Call pkg/config.GenerateConfig() -2. Create ConfigMap: `{name}-config-{hash[:8]}` -3. Set owner reference -4. Clean up old ConfigMaps (keep last 2) +- [ ] T045 (llama-stack-k8s-operator-onm.1) [US3] Write controller test: CR with models (default provider assignment) and tools, verify registered_resources in generated config in controllers/llamastackdistribution_controller_test.go +- [ ] T046 (llama-stack-k8s-operator-onm.2) [US3] Write controller test: CR with model specifying explicit provider assignment in controllers/llamastackdistribution_controller_test.go +- [ ] T047 (llama-stack-k8s-operator-onm.3) [US3] Write controller test: tools without toolRuntime provider fails with actionable error (FR-043) in controllers/llamastackdistribution_controller_test.go +- [ ] T048 (llama-stack-k8s-operator-onm.4) [US3] Create v1alpha2 sample CR: models and tools registration in config/samples/v1alpha2/with-resources.yaml -**Requirements covered**: FR-023, FR-024, FR-025, NFR-005 - -**Acceptance criteria**: -- [ ] Creates ConfigMap with hash-based name -- [ ] Sets owner reference correctly -- [ ] Cleans up old ConfigMaps -- [ ] Immutable pattern (new CM on changes) +**Checkpoint**: User Story 3 complete. Models, tools, and shields are registered in generated config. --- -### Task 3.4: Extend ManifestContext for Config +## Phase 6: User Story 4 - State Storage Configuration (Priority: P1) -**Priority**: P1 -**Blocked by**: 3.3 +**Goal**: Configure PostgreSQL/Redis for state storage via CR spec -**Description**: -Extend ManifestContext to include generated config information. +**Independent Test**: Deploy a LLSD CR with `storage.sql` configuration, verify the server uses PostgreSQL for state storage -**File**: `pkg/deploy/kustomizer.go` +### Implementation for User Story 4 -**New fields**: -```go -type ManifestContext struct { - // ... existing fields - GeneratedConfigMapName string - GeneratedConfigHash string - SecretEnvVars []corev1.EnvVar -} -``` +- [ ] T049 (llama-stack-k8s-operator-l4o.1) [US4] Write controller test: CR with postgres storage and secretKeyRef, verify storage section in config and env vars on Deployment in controllers/llamastackdistribution_controller_test.go +- [ ] T050 (llama-stack-k8s-operator-l4o.2) [US4] Write controller test: CR with redis kv storage, verify endpoint in config in controllers/llamastackdistribution_controller_test.go +- [ ] T051 (llama-stack-k8s-operator-l4o.3) [US4] Write controller test: CR without storage section preserves distribution defaults in controllers/llamastackdistribution_controller_test.go +- [ ] T052 (llama-stack-k8s-operator-l4o.4) [US4] Create v1alpha2 sample CR: postgres state storage in config/samples/v1alpha2/with-postgres-storage.yaml -**Requirements covered**: FR-026 - -**Acceptance criteria**: -- [ ] ManifestContext includes new fields -- [ ] Deployment template uses new fields -- [ ] Hash annotation triggers rollouts +**Checkpoint**: User Story 4 complete. Storage configuration generates correct config.yaml sections. --- -### Task 3.5: Implement Networking Configuration +## Phase 7: User Story 8 - Runtime Configuration Updates (Priority: P1) -**Priority**: P1 -**Blocked by**: 3.1 +**Goal**: Update LLSD CR and have running instance pick up changes automatically -**Description**: -Implement networking spec handling (port, TLS, expose, allowedFrom). +**Independent Test**: Deploy a LLSD CR, wait for Ready, modify the CR's providers section, verify the Pod restarts with the updated config.yaml -**Functions**: -- `GetServerPort(spec) int32` -- `ShouldExposeRoute(spec) bool` -- `GetExposeHostname(spec) string` -- `GetTLSConfig(spec) *TLSConfig` -- `GetCABundleVolume(spec) (*corev1.Volume, *corev1.VolumeMount)` - FR-063 -- Extend existing Ingress reconciliation +### Implementation for User Story 8 -**Requirements covered**: FR-060 to FR-066 +- [ ] T053 (llama-stack-k8s-operator-9v9.1) [US8] Implement no-op detection: skip Pod restart when generated config.yaml content hash is identical (FR-096) in controllers/configmap_reconciler.go +- [ ] T054 (llama-stack-k8s-operator-9v9.2) [US8] Implement failure preservation: on config generation error, keep current running Deployment unchanged, set ConfigGenerated=False (FR-097) in controllers/llamastackdistribution_controller.go +- [ ] T055 (llama-stack-k8s-operator-9v9.3) [US8] Wire DeploymentUpdated and SecretsResolved status conditions in controllers/status.go +- [ ] T056 (llama-stack-k8s-operator-9v9.4) [US8] Write controller test: modify CR providers, verify new ConfigMap created and Deployment updated with new hash annotation in controllers/llamastackdistribution_controller_test.go +- [ ] T057 (llama-stack-k8s-operator-9v9.5) [US8] Write controller test: modify CR with identical config output, verify no Pod restart in controllers/llamastackdistribution_controller_test.go +- [ ] T058 (llama-stack-k8s-operator-9v9.6) [US8] Write controller test: modify CR with invalid config, verify running Deployment preserved and error in status in controllers/llamastackdistribution_controller_test.go -**Acceptance criteria**: -- [ ] Port defaults to 8321 -- [ ] Polymorphic expose works (bool and object) -- [ ] TLS configuration applied (FR-061, FR-062) -- [ ] CA bundle mounted from ConfigMap when specified (FR-063) -- [ ] Ingress/Route created with correct hostname (FR-064, FR-065) -- [ ] NetworkPolicy configured for allowedFrom (FR-066) +**Checkpoint**: User Story 8 complete. Runtime updates work with no-op detection and failure preservation. --- -### Task 3.6: Implement Validation +## Phase 8: User Story 5 - Network Exposure Configuration (Priority: P2) -**Priority**: P1 -**Blocked by**: 3.1 +**Goal**: Expose llama-stack service externally with TLS -**Description**: -Implement controller-level validation for secret and ConfigMap references. +**Independent Test**: Deploy a LLSD CR with `networking.expose: {enabled: true}` and `networking.tls`, verify Ingress/Route is created with TLS configured -**Functions**: -- `ValidateSecretReferences(ctx, spec, namespace) error` -- `ValidateConfigMapReferences(ctx, spec, namespace) error` -- `ValidateProviderReferences(spec) error` +### Implementation for User Story 5 -**Requirements covered**: FR-073, FR-074, FR-075 +- [ ] T059 (llama-stack-k8s-operator-khz.1) [US5] Implement networking overrides: apply port from spec to server config and Deployment containerPort in controllers/v1alpha2_helpers.go +- [ ] T060 (llama-stack-k8s-operator-khz.2) [US5] Extend existing Ingress/Route reconciliation for v1alpha2 ExposeConfig (enabled + hostname fields) in controllers/network_resources.go +- [ ] T061 (llama-stack-k8s-operator-khz.3) [US5] Write controller test: CR with expose enabled creates Ingress with auto-generated hostname in controllers/network_resources_test.go +- [ ] T062 (llama-stack-k8s-operator-khz.4) [US5] Write controller test: CR with expose hostname creates Ingress with specified hostname in controllers/network_resources_test.go +- [ ] T063 (llama-stack-k8s-operator-khz.5) [US5] Create v1alpha2 sample CR: TLS and expose configuration in config/samples/v1alpha2/with-networking.yaml -**Acceptance criteria**: -- [ ] Validates all secretKeyRef references -- [ ] Validates overrideConfig and caBundle references -- [ ] Validates model โ†’ provider references -- [ ] Error messages include field paths +**Checkpoint**: User Story 5 complete. Networking exposure works with TLS. --- -### Task 3.7: Implement Spec 001 Integration - -**Priority**: P1 -**Blocked by**: 3.3 - -**Description**: -Implement merging of external providers from spec 001. +## Phase 9: User Story 6 - Full ConfigMap Override (Priority: P2) -**Function**: `MergeExternalProviders(generated, external) (*MergedConfig, []string)` +**Goal**: Power users can provide their own complete config.yaml via ConfigMap -**Logic**: -1. Take generated config as base -2. Add external providers -3. Log warnings for ID conflicts -4. Return merged config and warnings +**Independent Test**: Deploy a LLSD CR with `overrideConfig.configMapName`, verify the server uses the ConfigMap contents -**Requirements covered**: FR-090, FR-091, FR-092 +### Implementation for User Story 6 -**Acceptance criteria**: -- [ ] External providers added to generated config -- [ ] ID conflicts logged as warnings -- [ ] External providers override inline +- [ ] T064 (llama-stack-k8s-operator-8xi.1) [US6] Implement overrideConfig path: use referenced ConfigMap contents as config.yaml directly, skip generation in controllers/v1alpha2_helpers.go +- [ ] T065 (llama-stack-k8s-operator-8xi.2) [US6] Write controller test: CR with overrideConfig uses referenced ConfigMap in controllers/llamastackdistribution_controller_test.go +- [ ] T066 (llama-stack-k8s-operator-8xi.3) [US6] Write controller test: CR with both providers and overrideConfig rejected by CEL in controllers/llamastackdistribution_controller_test.go +- [ ] T067 (llama-stack-k8s-operator-8xi.4) [US6] Create v1alpha2 sample CR: full ConfigMap override in config/samples/v1alpha2/with-override-config.yaml ---- - -### Task 3.8: Extend Status Reporting - -**Priority**: P1 -**Blocked by**: 3.3 - -**Description**: -Add config generation status fields and conditions. - -**File**: `controllers/status.go` - -**New conditions**: -- `ConfigGenerated`: True when config successfully generated -- `DeploymentUpdated`: True when Deployment spec updated with current config -- `Available`: True when at least one Pod is ready with current config -- `SecretsResolved`: True when all secret references valid - -**New status fields**: -```go -type ConfigGenerationStatus struct { - ConfigMapName string - GeneratedAt metav1.Time - ProviderCount int - ResourceCount int - ConfigVersion int -} - -type ResolvedDistributionStatus struct { - Image string // Resolved image from distribution.name - ConfigSource string // "embedded" or "oci-label" - ConfigHash string // Hash of current base config -} -``` - -**Requirements covered**: FR-020a, FR-099 - -**Acceptance criteria**: -- [ ] New conditions set correctly -- [ ] Config generation details in status -- [ ] `resolvedDistribution` recorded in status -- [ ] Status updated on each reconcile +**Checkpoint**: User Story 6 complete. Override escape hatch works. --- -### Task 3.9: Implement Validating Admission Webhook - -**Priority**: P1 -**Blocked by**: 1.7 - -**Description**: -Implement a validating webhook for constraints that cannot be expressed in CEL, complementing the CEL rules added in Phase 1 (Task 1.7). +## Phase 10: User Story 7 - Migration from v1alpha1 (Priority: P2) -**File**: `api/v1alpha2/llamastackdistribution_webhook.go` +**Goal**: Existing v1alpha1 CRs continue working after operator upgrade -**Validation logic**: -- Verify referenced Secrets exist in the namespace (fast admission-time feedback) -- Verify referenced ConfigMaps exist for `overrideConfig` and `caBundle` -- Validate provider ID references in `resources.models[].provider` -- Cross-field semantic validation (e.g., model provider references valid provider IDs) +**Independent Test**: Apply a v1alpha1 CR, upgrade operator, verify the CR continues to work and can be retrieved as v1alpha2 -**Configuration**: -- Webhook deployment via kustomize manifests (`config/webhook/`) -- Certificate management using cert-manager or operator-managed self-signed certs -- Failure policy: `Fail` (reject CR if webhook unreachable) +### Implementation for User Story 7 -```go -func (r *LlamaStackDistribution) ValidateCreate() (admission.Warnings, error) { - return r.validate() -} +- [ ] T068 (llama-stack-k8s-operator-wwz.1) [US7] Implement v1alpha2 hub conversion marker in api/v1alpha2/llamastackdistribution_conversion.go +- [ ] T069 (llama-stack-k8s-operator-wwz.2) [US7] Implement v1alpha1 spoke ConvertTo (v1alpha1 โ†’ v1alpha2) with field mapping per migration table in api/v1alpha1/llamastackdistribution_conversion.go +- [ ] T070 (llama-stack-k8s-operator-wwz.3) [US7] Implement v1alpha1 spoke ConvertFrom (v1alpha2 โ†’ v1alpha1) with annotation preservation for v1alpha2-only fields in api/v1alpha1/llamastackdistribution_conversion.go +- [ ] T071 (llama-stack-k8s-operator-wwz.4) [US7] Write round-trip conversion tests: v1alpha1 โ†’ v1alpha2 โ†’ v1alpha1, verify field preservation in api/v1alpha1/llamastackdistribution_conversion_test.go +- [ ] T072 (llama-stack-k8s-operator-wwz.5) [US7] Write conversion test: v1alpha2 with new fields โ†’ v1alpha1 โ†’ v1alpha2, verify annotation round-trip in api/v1alpha1/llamastackdistribution_conversion_test.go +- [ ] T073 (llama-stack-k8s-operator-wwz.6) [US7] Move existing v1alpha1 sample CRs to config/samples/v1alpha1/ subdirectory -func (r *LlamaStackDistribution) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - return r.validate() -} - -func (r *LlamaStackDistribution) validate() (admission.Warnings, error) { - var allErrs field.ErrorList - // Validate secret references exist - // Validate ConfigMap references exist - // Validate provider ID cross-references - return nil, allErrs.ToAggregate() -} -``` - -**Requirements covered**: FR-076, FR-077, FR-078 - -**Acceptance criteria**: -- [ ] Webhook validates Secret existence at admission time -- [ ] Webhook validates ConfigMap references -- [ ] Webhook validates cross-field provider ID references -- [ ] Clear error messages with field paths -- [ ] Webhook kustomize manifests configured +**Checkpoint**: User Story 7 complete. v1alpha1 CRs continue working via conversion webhook. --- -### Task 3.10: Implement Runtime Configuration Update Logic - -**Priority**: P1 -**Blocked by**: 3.3 - -**Description**: -On every reconciliation, compare the generated config hash with the currently deployed config hash. Only update the Deployment when content actually changes. On failure, preserve the current running Deployment. - -**File**: `controllers/llamastackdistribution_controller.go` +## Phase 11: Webhooks & Deployment -**Logic**: -``` -Reconcile() -โ”œโ”€โ”€ Generate config (or use overrideConfig) -โ”œโ”€โ”€ Compute content hash of generated config -โ”œโ”€โ”€ Compare with status.configGeneration.configMapName hash -โ”œโ”€โ”€ If identical โ†’ skip update, no Pod restart -โ””โ”€โ”€ If different: - โ”œโ”€โ”€ Create new ConfigMap - โ”œโ”€โ”€ Update Deployment atomically (image + config + env) - โ”œโ”€โ”€ On success โ†’ update status - โ””โ”€โ”€ On failure โ†’ preserve current Deployment, report error -``` - -**Failure preservation**: -```go -func (r *Reconciler) reconcileConfig(ctx context.Context, instance *v1alpha2.LlamaStackDistribution) error { - generated, err := config.GenerateConfig(ctx, instance.Spec, resolvedImage) - if err != nil { - // Preserve current running state, report error - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: "ConfigGenerated", - Status: metav1.ConditionFalse, - Reason: "ConfigGenerationFailed", - Message: err.Error(), - }) - return nil // Don't requeue, let user fix the CR - } - // ... proceed with update -} -``` +**Purpose**: Validating webhook and kustomize deployment manifests -**Requirements covered**: FR-095, FR-096, FR-097 +- [ ] T074 (llama-stack-k8s-operator-7hy.1) Implement validating webhook: distribution name validation against distributions.json in api/v1alpha2/llamastackdistribution_webhook.go +- [ ] T075 (llama-stack-k8s-operator-7hy.2) Implement validating webhook: Secret existence checks for all secretKeyRef references in api/v1alpha2/llamastackdistribution_webhook.go +- [ ] T076 (llama-stack-k8s-operator-7hy.3) Implement validating webhook: ConfigMap existence for overrideConfig and caBundle in api/v1alpha2/llamastackdistribution_webhook.go +- [ ] T077 (llama-stack-k8s-operator-7hy.4) Implement validating webhook: provider ID cross-references in resources.models[].provider in api/v1alpha2/llamastackdistribution_webhook.go +- [ ] T078 (llama-stack-k8s-operator-7hy.5) Implement validating webhook: disabled+provider conflict check in api/v1alpha2/llamastackdistribution_webhook.go +- [ ] T079 (llama-stack-k8s-operator-7hy.6) [P] Write webhook tests: valid CRs accepted, invalid distribution name rejected with available names in api/v1alpha2/llamastackdistribution_webhook_test.go +- [ ] T080 (llama-stack-k8s-operator-7hy.7) [P] Write webhook tests: missing Secret rejected, missing ConfigMap rejected, invalid provider reference rejected in api/v1alpha2/llamastackdistribution_webhook_test.go +- [ ] T081 (llama-stack-k8s-operator-7hy.8) [P] Create cert-manager manifests for webhook TLS in config/certmanager/ +- [ ] T082 (llama-stack-k8s-operator-7hy.9) [P] Create CRD webhook patch in config/crd/patches/webhook_in_llamastackdistributions.yaml +- [ ] T083 (llama-stack-k8s-operator-7hy.10) [P] Create manager webhook volume patch in config/default/manager_webhook_patch.yaml +- [ ] T084 (llama-stack-k8s-operator-7hy.11) Update config/default/kustomization.yaml to enable webhook and certmanager +- [ ] T085 (llama-stack-k8s-operator-7hy.12) [P] Create OpenShift overlay for service-ca webhook certificates in config/openshift/ -**Acceptance criteria**: -- [ ] Config hash comparison prevents unnecessary restarts -- [ ] Failed config generation preserves current Deployment -- [ ] Error reported in status conditions on failure -- [ ] Successful update reflected in status +**Checkpoint**: Webhooks deployed, admission validation working. --- -### Task 3.11: Implement Atomic Deployment Updates - -**Priority**: P1 -**Blocked by**: 3.10 - -**Description**: -When the Deployment needs updating, apply all changes (image, ConfigMap mount, env vars, hash annotation) in a single `client.Update()` call to prevent intermediate states where the running image and config are mismatched. - -**File**: `controllers/llamastackdistribution_controller.go` - -```go -func (r *Reconciler) updateDeploymentAtomically( - ctx context.Context, - deployment *appsv1.Deployment, - resolvedImage string, - configMapName string, - envVars []corev1.EnvVar, - configHash string, -) error { - // Update all fields in one mutation - deployment.Spec.Template.Spec.Containers[0].Image = resolvedImage - // ... update ConfigMap volume, env vars, hash annotation - return r.Client.Update(ctx, deployment) -} -``` - -**Operator upgrade handling**: When the operator is upgraded and `distributions.json` maps a name to a new image, the reconciler detects the image change via `status.resolvedDistribution.image` comparison and triggers an atomic update with the new base config. +## Phase 12: Polish & Cross-Cutting Concerns -**Requirements covered**: FR-098, FR-100, FR-101 +**Purpose**: E2E tests, documentation, cleanup -**Acceptance criteria**: -- [ ] Image + ConfigMap + env vars updated in single API call -- [ ] No intermediate state where image and config mismatch -- [ ] Operator upgrade triggers atomic update when image changes -- [ ] Failed update preserves current Deployment (see FR-097) +- [ ] T086 (llama-stack-k8s-operator-app.1) [P] Write E2E test: deploy v1alpha2 CR with inference provider, verify server starts and responds in tests/e2e/ +- [ ] T087 (llama-stack-k8s-operator-app.2) [P] Write E2E test: update v1alpha2 CR, verify rolling update with new config in tests/e2e/ +- [ ] T088 (llama-stack-k8s-operator-app.3) [P] Create v1alpha2 migration documentation in docs/migration-v1alpha1-to-v1alpha2.md +- [ ] T089 (llama-stack-k8s-operator-app.4) Update config/samples/kustomization.yaml to reference new v1alpha2 samples +- [ ] T090 (llama-stack-k8s-operator-app.5) Wire Available status condition based on Deployment readiness in controllers/status.go +- [ ] T091 (llama-stack-k8s-operator-app.6) Run quickstart.md validation: verify all 7 examples parse as valid YAML and match CRD schema --- -### Task 3.12: Implement Distribution Resolution Tracking - -**Priority**: P1 -**Blocked by**: 3.3 +## Dependencies & Execution Order -**Description**: -Track the resolved image in `status.resolvedDistribution` so the controller can detect changes across reconciliations (e.g., after operator upgrade where `distributions.json` maps a name to a new image). +### Phase Dependencies -**File**: `controllers/llamastackdistribution_controller.go` +- **Setup (Phase 1)**: No dependencies, can start immediately +- **Foundational (Phase 2)**: Depends on Setup completion, BLOCKS all user stories +- **User Stories (Phases 3-10)**: All depend on Foundational phase completion + - US1 (Phase 3): No story dependencies, MVP target + - US2 (Phase 4): Builds on US1 controller (same file, sequential) + - US3 (Phase 5): Builds on US1 controller (same file, sequential) + - US4 (Phase 6): Builds on US1 controller (same file, sequential) + - US8 (Phase 7): Builds on US1 controller (same file, sequential) + - US5 (Phase 8): Independent from US2-US4 (different files), can parallel with US2+ + - US6 (Phase 9): Builds on US1 controller (same file), can parallel with US5 + - US7 (Phase 10): Independent (different package), can parallel with US2+ +- **Webhooks (Phase 11)**: Depends on Phase 1 types, can parallel with US2+ +- **Polish (Phase 12)**: Depends on all phases -**Logic**: -1. Resolve `distribution.name` to concrete image using `distributions.json` + `image-overrides` -2. Compare with `status.resolvedDistribution.image` -3. If different: regenerate config with new base, update atomically -4. Record new resolved image in status +### Parallel Opportunities -**Requirements covered**: FR-020a, FR-020b, FR-020c - -**Acceptance criteria**: -- [ ] Resolved image recorded in `status.resolvedDistribution` -- [ ] Image change detected between reconciliations -- [ ] Image change triggers config regeneration and atomic update -- [ ] Config source ("embedded" or "oci-label") recorded in status +After Phase 2 (Foundational), these can run in parallel: +- **Stream A**: US1 โ†’ US2 โ†’ US3 โ†’ US4 โ†’ US8 (controller modifications, sequential) +- **Stream B**: US7 (conversion webhook, separate package) +- **Stream C**: Phase 11 webhooks (separate package) +- **Stream D**: US5 (networking, mostly separate files) --- -## Phase 4: Conversion Webhook - -### Task 4.1: Implement v1alpha2 Hub - -**Priority**: P2 -**Blocked by**: Phase 1 - -**Description**: -Mark v1alpha2 as the conversion hub (storage version). - -**File**: `api/v1alpha2/llamastackdistribution_conversion.go` - -**Implementation**: - -In controller-runtime, the Hub only implements a marker method. Conversion logic lives on the Spoke (v1alpha1). +## Parallel Example: Phase 2 (Foundational) -```go -// Hub marks v1alpha2 as the storage version for conversion. -func (dst *LlamaStackDistribution) Hub() {} +```bash +# These can all run in parallel (different files): +Task: "Implement EmbeddedConfigResolver in pkg/config/resolver.go" +Task: "Create embedded config for starter in pkg/deploy/configs/starter/config.yaml" +Task: "Implement provider expansion in pkg/config/provider.go" +Task: "Implement secret collection in pkg/config/secret.go" +Task: "Implement resource expansion in pkg/config/resource.go" +Task: "Implement storage config in pkg/config/storage.go" +Task: "Implement config version detection in pkg/config/version.go" + +# Then sequentially (depends on above): +Task: "Implement merge logic in pkg/config/merge.go" +Task: "Implement GenerateConfig orchestrator in pkg/config/generator.go" ``` -**Requirements covered**: FR-081 - -**Acceptance criteria**: -- [ ] v1alpha2 implements `conversion.Hub` interface via `Hub()` marker method -- [ ] No conversion logic on the hub (all conversion is on the v1alpha1 spoke) - --- -### Task 4.2: Implement v1alpha1 to v1alpha2 Conversion +## Implementation Strategy -**Priority**: P2 -**Blocked by**: 4.1 +### MVP First (User Story 1 Only) -**Description**: -Implement conversion from v1alpha1 to v1alpha2. +1. Complete Phase 1: Setup (v1alpha2 types + CEL rules) +2. Complete Phase 2: Foundational (config generation pipeline) +3. Complete Phase 3: User Story 1 (controller integration) +4. **STOP and VALIDATE**: Deploy minimal v1alpha2 CR, verify config.yaml generated and server starts +5. This is a deployable, testable increment -**File**: `api/v1alpha1/llamastackdistribution_conversion.go` +### Incremental Delivery -**Field mapping**: -- `spec.server.distribution` โ†’ `spec.distribution` -- `spec.server.containerSpec.port` โ†’ `spec.networking.port` -- `spec.server.containerSpec.resources` โ†’ `spec.workload.resources` -- (see spec for full mapping table) +1. Setup + Foundational โ†’ Config pipeline works in isolation +2. Add US1 โ†’ Minimal inference config works end-to-end (MVP!) +3. Add US2-US4 โ†’ Multiple providers, resources, storage +4. Add US8 โ†’ Runtime updates with no-op detection +5. Add US5-US6 โ†’ Networking and override escape hatch +6. Add US7 โ†’ v1alpha1 backward compatibility +7. Add webhooks โ†’ Admission-time validation +8. Polish โ†’ E2E tests, docs, cleanup -**Requirements covered**: FR-080, FR-083 +### PR Strategy -**Acceptance criteria**: -- [ ] All v1alpha1 fields converted correctly -- [ ] No data loss for existing fields -- [ ] New fields have sensible defaults +Split into focused PRs aligned with phases: +- **PR 1**: Phase 1 + Phase 2 (types + config pipeline, ~15 files) +- **PR 2**: Phase 3 (US1 controller integration, ~8 files) +- **PR 3**: Phases 4-7 (US2-US4, US8 controller tests, ~5 files) +- **PR 4**: Phases 8-10 (US5 networking, US6 override, US7 conversion, ~10 files) +- **PR 5**: Phase 11-12 (webhooks, e2e, docs, ~15 files) --- -### Task 4.3: Implement v1alpha2 to v1alpha1 Conversion - -**Priority**: P2 -**Blocked by**: 4.1 - -**Description**: -Implement conversion from v1alpha2 to v1alpha1 (where possible). - -**File**: `api/v1alpha1/llamastackdistribution_conversion.go` - -**Notes**: -- New fields (providers, resources, storage) cannot be represented in v1alpha1 -- These are lost in down-conversion -- Log warnings for data loss +## Notes -**Requirements covered**: FR-080, FR-082 +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently +- Config generation pipeline (Phase 2) is pure Go with no K8s dependencies, enabling fast unit tests -**Acceptance criteria**: -- [ ] Mappable fields converted correctly -- [ ] New fields handled gracefully (ignored with warning) -- [ ] Existing v1alpha1 CRs continue working +## Beads Task Management ---- - -### Task 4.4: Configure and Test Webhook - -**Priority**: P2 -**Blocked by**: 4.2, 4.3 - -**Description**: -Configure conversion webhook and test thoroughly. - -**Files**: -- `config/webhook/manifests.yaml` -- `main.go` - -**Tests**: -- v1alpha1 โ†’ v1alpha2 โ†’ v1alpha1 round-trip -- v1alpha2 โ†’ v1alpha1 โ†’ v1alpha2 round-trip -- Edge cases (empty fields, defaults) - -**Acceptance criteria**: -- [ ] Webhook registered and working -- [ ] Round-trip conversion works -- [ ] No data loss for v1alpha1 fields - ---- - -## Phase 5: Testing & Documentation - -### Task 5.1: Unit Tests for Config Package - -**Priority**: P2 -**Blocked by**: Phase 2 - -**Description**: -Write comprehensive unit tests for pkg/config. - -**Files**: -- `pkg/config/config_test.go` -- `pkg/config/provider_test.go` -- `pkg/config/resource_test.go` -- `pkg/config/storage_test.go` -- `pkg/config/secret_resolver_test.go` -- `pkg/config/version_test.go` - -**Coverage target**: >80% - -**Acceptance criteria**: -- [ ] All functions tested -- [ ] Edge cases covered -- [ ] Determinism verified - ---- - -### Task 5.2: Integration Tests for Controller - -**Priority**: P2 -**Blocked by**: Phase 3 - -**Description**: -Write integration tests for v1alpha2 controller logic. - -**File**: `controllers/llamastackdistribution_v1alpha2_test.go` - -**Test scenarios**: -- Simple inference configuration -- Multiple providers -- Resource registration -- State storage configuration -- Network exposure -- Override config -- Validation errors -- Runtime config updates (US8): CR update triggers config regeneration -- Atomic Deployment updates: image + config updated together -- Webhook validation: invalid references rejected at admission -- Distribution resolution tracking: operator upgrade triggers update -- Config generation failure: current Deployment preserved - -**Acceptance criteria**: -- [ ] All user stories have tests (including US8) -- [ ] Edge cases covered -- [ ] Error scenarios tested -- [ ] Webhook validation tested -- [ ] Atomic update scenarios tested - ---- - -### Task 5.3: Conversion Tests - -**Priority**: P2 -**Blocked by**: Phase 4 - -**Description**: -Write tests for conversion webhook. - -**File**: `api/v1alpha2/conversion_test.go` - -**Test scenarios**: -- v1alpha1 โ†’ v1alpha2 for all field combinations -- v1alpha2 โ†’ v1alpha1 with data preservation -- Round-trip conversion - -**Acceptance criteria**: -- [ ] All field mappings tested -- [ ] Data loss scenarios documented -- [ ] Round-trip works - ---- - -### Task 5.4: Sample Manifests - -**Priority**: P2 -**Blocked by**: Phase 3 - -**Description**: -Create sample v1alpha2 manifests for users. - -**Files**: -- `config/samples/v1alpha2-simple.yaml` -- `config/samples/v1alpha2-full.yaml` -- `config/samples/v1alpha2-postgres.yaml` -- `config/samples/v1alpha2-multi-provider.yaml` -- `config/samples/v1alpha2-override.yaml` - -**Acceptance criteria**: -- [ ] Samples are valid and deploy successfully -- [ ] Cover common use cases -- [ ] Include inline comments - ---- - -### Task 5.5: Documentation - -**Priority**: P2 -**Blocked by**: Phase 3, Phase 4 - -**Description**: -Update documentation for v1alpha2. - -**Files**: -- `README.md` - Add v1alpha2 overview -- `docs/configuration.md` - Detailed config guide -- `docs/migration-v1alpha1-to-v1alpha2.md` - Migration guide -- `docs/api-reference.md` - API reference update - -**Acceptance criteria**: -- [ ] All new features documented -- [ ] Migration path clear -- [ ] Examples included - ---- - -### Task 5.6: Performance Benchmarks - -**Priority**: P2 -**Blocked by**: 2.8 - -**Description**: -Write Go benchmark tests to verify config generation completes within the NFR-002 threshold (5 seconds for typical configurations). - -**File**: `pkg/config/config_benchmark_test.go` - -**Benchmark scenarios**: -- Single provider, single model (minimal config) -- 5 providers, 10 models, storage, networking (typical production) -- 10 providers, 50 models, all features enabled (stress test) - -**Requirements covered**: NFR-002 - -**Acceptance criteria**: -- [ ] Benchmark tests pass under 5 seconds for typical configuration -- [ ] Results documented in test output -- [ ] CI runs benchmarks (optional, for regression detection) - ---- - -## Task Dependencies Graph - -``` -Phase 1 (CRD Schema) -โ”œโ”€โ”€ 1.1 โ”€โ–บ 1.2, 1.3, 1.4, 1.5, 1.6 -โ”œโ”€โ”€ 1.2 โ”€โ” -โ”œโ”€โ”€ 1.3 โ”€โ”ค -โ”œโ”€โ”€ 1.4 โ”€โ”ผโ”€โ–บ 1.7 โ”€โ–บ 1.8 -โ”œโ”€โ”€ 1.5 โ”€โ”ค -โ””โ”€โ”€ 1.6 โ”€โ”˜ - -Phase 2 (Config Generation) -โ”œโ”€โ”€ 2.1 โ”€โ–บ 2.2, 2.4, 2.6 -โ”œโ”€โ”€ 2.2 โ”€โ–บ 2.3 -โ”œโ”€โ”€ 2.4 โ”€โ” -โ”œโ”€โ”€ 2.5 โ”€โ”ผโ”€โ–บ 2.7 โ”€โ–บ 2.8 -โ””โ”€โ”€ 2.6 โ”€โ”˜ - -Phase 3 (Controller) -โ”œโ”€โ”€ 3.1 โ”€โ–บ 3.2, 3.5, 3.6 -โ”œโ”€โ”€ 3.2 โ”€โ–บ 3.3 -โ”œโ”€โ”€ 3.3 โ”€โ–บ 3.4, 3.7, 3.8, 3.10, 3.12 -โ”œโ”€โ”€ 3.10 โ”€โ–บ 3.11 -โ”œโ”€โ”€ 3.9 (blocked by 1.7) -โ””โ”€โ”€ 3.12 (parallel with 3.10) - -Phase 4 (Webhook) -โ””โ”€โ”€ 4.1 โ”€โ–บ 4.2, 4.3 โ”€โ–บ 4.4 - -Phase 5 (Testing) -โ”œโ”€โ”€ 5.1 (blocked by Phase 2) -โ”œโ”€โ”€ 5.2 (blocked by Phase 3) -โ”œโ”€โ”€ 5.3 (blocked by Phase 4) -โ”œโ”€โ”€ 5.4, 5.5 (blocked by Phase 3, 4) -โ””โ”€โ”€ 5.6 (blocked by 2.8) -``` +This project uses beads (`bd`) for persistent task tracking across sessions: +- Run `/sdd:beads-task-sync` to create bd issues from this file +- `bd ready --json` returns unblocked tasks (dependencies resolved) +- `bd close ` marks a task complete (use `-r "reason"` for close reason, NOT `--comment`) +- `bd comments add "text"` adds a detailed comment to an issue +- `bd backup` persists state to git +- `bd create "DISCOVERED: [short title]" --labels discovered` tracks new work + - Keep titles crisp (under 80 chars); add details via `bd comments add "details"` +- Run `/sdd:beads-task-sync --reverse` to update checkboxes from bd state +- **Always use `jq` to parse bd JSON output, NEVER inline Python one-liners** From b594f06e89a4f3a1a68c999562ef9cc7e000c216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Mar 2026 07:31:13 +0100 Subject: [PATCH 6/9] fix: quote Go type annotations in crd-schema.yaml for YAML validity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The []ProviderConfig type annotations were interpreted as YAML flow sequences by the pre-commit YAML validator. Quoting them preserves readability while keeping the file valid YAML. Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../contracts/crd-schema.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/002-operator-generated-config/contracts/crd-schema.yaml b/specs/002-operator-generated-config/contracts/crd-schema.yaml index 687842789..282fd25b6 100644 --- a/specs/002-operator-generated-config/contracts/crd-schema.yaml +++ b/specs/002-operator-generated-config/contracts/crd-schema.yaml @@ -16,11 +16,11 @@ spec: # --- Providers (optional, mutually exclusive with overrideConfig) --- providers: - inference: []ProviderConfig # Always a list (single provider = one-element list) - safety: []ProviderConfig - vectorIo: []ProviderConfig - toolRuntime: []ProviderConfig - telemetry: []ProviderConfig + inference: "[]ProviderConfig" # Always a list (single provider = one-element list) + safety: "[]ProviderConfig" + vectorIo: "[]ProviderConfig" + toolRuntime: "[]ProviderConfig" + telemetry: "[]ProviderConfig" # ProviderConfig schema: # - id: string (required when list has >1 element; auto-generated for single-element lists) From 7e17894500246b552d8fe1f361070931b69ca212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Mar 2026 13:46:27 +0100 Subject: [PATCH 7/9] docs(spec): explicitly call out models and expose type changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review summary mentioned providers polymorphism removal but didn't explicitly list the models and expose changes as separate line items. Added both to the "Key Changes from PR #253" table and expanded the "Decision 1" section to cover all three polymorphic field replacements. Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../002-operator-generated-config/review_summary.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/specs/002-operator-generated-config/review_summary.md b/specs/002-operator-generated-config/review_summary.md index 7485d1e34..7e254c420 100644 --- a/specs/002-operator-generated-config/review_summary.md +++ b/specs/002-operator-generated-config/review_summary.md @@ -26,9 +26,15 @@ Key design decision to validate: **providers are always lists** (e.g., `inferenc These are the decisions that will be hardest to change after implementation starts: -**Decision 1: Typed slices instead of polymorphic JSON** (spec.md FR-004, research.md R1) +**Decision 1: Typed structs instead of polymorphic JSON** (spec.md FR-004, FR-007, FR-011, research.md R1) -The original PR #253 used `apiextensionsv1.JSON` for polymorphic fields (object OR list). This caused: no kubebuilder validation, impossible CEL rules, ~500 lines of parsing code, false-positive secret detection bugs. The new design uses `[]ProviderConfig` everywhere. Tradeoff: users always write list syntax. Verify this is acceptable. +The original PR #253 used `apiextensionsv1.JSON` for three polymorphic fields. All three are replaced with typed alternatives: + +- **Providers** (`ProviderConfigOrList`): Now `[]ProviderConfig`. Users always write list syntax (FR-004). +- **Models** (`[]apiextensionsv1.JSON`): Now `[]ModelConfig` with only `name` required (FR-007). Users write `- name: "llama3.2-8b"` instead of `- "llama3.2-8b"`. +- **Expose** (`*apiextensionsv1.JSON`): Now `ExposeConfig` struct with `enabled` bool + `hostname` string (FR-011). Users write `expose: {enabled: true}` instead of `expose: true`. + +This eliminates: no kubebuilder validation, impossible CEL rules, ~500 lines of parsing code, false-positive secret detection bugs. Verify the tradeoffs are acceptable. **Decision 2: Explicit `secretRefs` field instead of heuristic detection** (spec.md FR-005, contracts/config-generation.yaml) @@ -72,6 +78,8 @@ This spec addresses all critical issues raised in the PR #253 review: | PR #253 Issue | Resolution | Spec Location | |--------------|------------|---------------| | Polymorphic JSON types lose kubebuilder validation | Replaced with typed `[]ProviderConfig` slices | FR-004, research.md R1 | +| Polymorphic models (`[]apiextensionsv1.JSON`) | Replaced with typed `[]ModelConfig` (only `name` required) | FR-007, data-model.md ModelConfig | +| Polymorphic expose (`*apiextensionsv1.JSON`) | Replaced with typed `ExposeConfig` struct (`enabled` bool + `hostname` string) | FR-011, data-model.md ExposeConfig | | CEL rules impossible on `apiextensionsv1.JSON` | CEL now works because providers are typed | FR-071, FR-072 | | `extractDirectSecretRef` false positives | Explicit `secretRefs` field, no heuristic matching | FR-005 | | `sortedMapKeys` doesn't sort | Determinism addressed in NFR-001, merge.go | NFR-001 | From 9c9dd0bc438d2a4675e4701cc1b77c4cbae4b973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Mar 2026 14:21:42 +0100 Subject: [PATCH 8/9] docs(spec): rename review_summary.md to REVIEW.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old name implied "a summary of a review that happened" when the file is actually "the document that guides reviewers through their review." REVIEW.md follows the convention of README.md, CHANGELOG.md, and CONTRIBUTING.md, making it immediately recognizable as the entry point for reviewers. Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- .../{review_summary.md => REVIEW.md} | 0 specs/002-operator-generated-config/plan.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename specs/002-operator-generated-config/{review_summary.md => REVIEW.md} (100%) diff --git a/specs/002-operator-generated-config/review_summary.md b/specs/002-operator-generated-config/REVIEW.md similarity index 100% rename from specs/002-operator-generated-config/review_summary.md rename to specs/002-operator-generated-config/REVIEW.md diff --git a/specs/002-operator-generated-config/plan.md b/specs/002-operator-generated-config/plan.md index 0eec5360c..b1d7d3fd6 100644 --- a/specs/002-operator-generated-config/plan.md +++ b/specs/002-operator-generated-config/plan.md @@ -54,7 +54,7 @@ specs/002-operator-generated-config/ โ”œโ”€โ”€ research.md # Technical decisions and rationale โ”œโ”€โ”€ data-model.md # Entity definitions and relationships โ”œโ”€โ”€ quickstart.md # User-facing configuration examples -โ”œโ”€โ”€ review_summary.md # Executive brief +โ”œโ”€โ”€ REVIEW.md # Review guide for spec PR reviewers โ”œโ”€โ”€ contracts/ โ”‚ โ”œโ”€โ”€ config-generation.yaml # Config generator interface contract โ”‚ โ”œโ”€โ”€ crd-schema.yaml # CRD schema reference From c1b33cabaaedf1971efe14379b95b099c99783a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Tue, 10 Mar 2026 14:57:45 +0100 Subject: [PATCH 9/9] feat(api): introduce v1alpha2 CRD schema with typed provider slices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the complete v1alpha2 API types for LlamaStackDistribution: - ProvidersSpec with typed []ProviderConfig slices (no polymorphic JSON) - ProviderConfig with explicit secretRefs field (no heuristic detection) - ResourcesSpec with typed []ModelConfig (name required, provider optional) - StateStorageSpec with KV (sqlite/redis) and SQL (sqlite/postgres) backends - NetworkingSpec with typed ExposeConfig (enabled + hostname fields) - WorkloadSpec consolidating all deployment settings - Status types: ResolvedDistributionStatus, ConfigGenerationStatus - 4 condition types with reason constants and event types CEL validation rules (15 total): - 4 mutual exclusivity rules (providers/resources/storage/disabled vs overrideConfig) - 5 provider ID required rules (per API type, when list > 1) - 2 distribution name/image rules - TLS secretName required when enabled - Redis endpoint required, Postgres connectionString required Addresses FR-001 through FR-014, FR-070-072, FR-079, FR-079a-c from spec. Assisted-by: ๐Ÿค– Claude Code Signed-off-by: Roland HuรŸ --- api/v1alpha2/doc.go | 21 + api/v1alpha2/groupversion_info.go | 33 + api/v1alpha2/llamastackdistribution_types.go | 693 ++++ api/v1alpha2/zz_generated.deepcopy.go | 866 +++++ ...llamastack.io_llamastackdistributions.yaml | 3321 +++++++++++++++++ 5 files changed, 4934 insertions(+) create mode 100644 api/v1alpha2/doc.go create mode 100644 api/v1alpha2/groupversion_info.go create mode 100644 api/v1alpha2/llamastackdistribution_types.go create mode 100644 api/v1alpha2/zz_generated.deepcopy.go diff --git a/api/v1alpha2/doc.go b/api/v1alpha2/doc.go new file mode 100644 index 000000000..26876bccc --- /dev/null +++ b/api/v1alpha2/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha2 contains API Schema definitions for the v1alpha2 API group. +// This is the storage version and hub for conversion webhooks. +// +kubebuilder:object:generate=true +// +groupName=llamastack.io +package v1alpha2 diff --git a/api/v1alpha2/groupversion_info.go b/api/v1alpha2/groupversion_info.go new file mode 100644 index 000000000..cccadbac4 --- /dev/null +++ b/api/v1alpha2/groupversion_info.go @@ -0,0 +1,33 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "llamastack.io", Version: "v1alpha2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha2/llamastackdistribution_types.go b/api/v1alpha2/llamastackdistribution_types.go new file mode 100644 index 000000000..0bfe8e1c4 --- /dev/null +++ b/api/v1alpha2/llamastackdistribution_types.go @@ -0,0 +1,693 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + // DefaultContainerName is the default name for the container. + DefaultContainerName = "llama-stack" + // DefaultServerPort is the default port for the server. + DefaultServerPort int32 = 8321 + // DefaultServicePortName is the default name for the service port. + DefaultServicePortName = "http" + // DefaultLabelKey is the default key for labels. + DefaultLabelKey = "app" + // DefaultLabelValue is the default value for labels. + DefaultLabelValue = "llama-stack" + // DefaultMountPath is the default mount path for storage. + DefaultMountPath = "/.llama" + // LlamaStackDistributionKind is the kind name for LlamaStackDistribution resources. + LlamaStackDistributionKind = "LlamaStackDistribution" +) + +var ( + // DefaultStorageSize is the default size for persistent storage. + DefaultStorageSize = resource.MustParse("10Gi") + // DefaultServerCPURequest is the default CPU request for the server. + DefaultServerCPURequest = resource.MustParse("500m") + // DefaultServerMemoryRequest is the default memory request for the server. + DefaultServerMemoryRequest = resource.MustParse("1Gi") +) + +// --- Condition types, reasons, and event types --- + +const ( + // Condition types + ConditionTypeConfigGenerated = "ConfigGenerated" + ConditionTypeDeploymentUpdated = "DeploymentUpdated" + ConditionTypeAvailable = "Available" + ConditionTypeSecretsResolved = "SecretsResolved" + + // ConfigGenerated reasons + ReasonConfigGenerationSucceeded = "ConfigGenerationSucceeded" + ReasonConfigGenerationFailed = "ConfigGenerationFailed" + ReasonBaseConfigRequired = "BaseConfigRequired" + ReasonUnsupportedConfigVersion = "UnsupportedConfigVersion" + ReasonUpgradeConfigFailure = "UpgradeConfigFailure" + ReasonMissingProviderForResource = "MissingProviderForResource" + + // DeploymentUpdated reasons + ReasonDeploymentUpdateSucceeded = "DeploymentUpdateSucceeded" + ReasonDeploymentUpdateFailed = "DeploymentUpdateFailed" + ReasonDeploymentUpdateSkipped = "DeploymentUpdateSkipped" + + // Available reasons + ReasonMinimumReplicasAvailable = "MinimumReplicasAvailable" + ReasonReplicasUnavailable = "ReplicasUnavailable" + ReasonRolloutInProgress = "RolloutInProgress" + + // SecretsResolved reasons + ReasonAllSecretsFound = "AllSecretsFound" + ReasonSecretNotFound = "SecretNotFound" + ReasonSecretKeyMissing = "SecretKeyMissing" + + // Event types + EventConfigGenerated = "ConfigGenerated" + EventDeploymentUpdated = "DeploymentUpdated" + EventConfigGenerationFailed = "ConfigGenerationFailed" + EventSecretResolutionFailed = "SecretResolutionFailed" + + // Annotations + AnnotationConfigHash = "llamastack.io/config-hash" + AnnotationV1Alpha2Fields = "llamastack.io/v1alpha2-fields" + + // Labels + LabelManagedBy = "app.kubernetes.io/managed-by" + LabelComponent = "app.kubernetes.io/component" +) + +// DistributionPhase represents the current phase of the LlamaStackDistribution. +// +kubebuilder:validation:Enum=Pending;Initializing;Ready;Failed;Terminating +type DistributionPhase string + +const ( + PhasePending DistributionPhase = "Pending" + PhaseInitializing DistributionPhase = "Initializing" + PhaseReady DistributionPhase = "Ready" + PhaseFailed DistributionPhase = "Failed" + PhaseTerminating DistributionPhase = "Terminating" +) + +// --- Top-level CRD --- + +// LlamaStackDistributionSpec defines the desired state of LlamaStackDistribution. +// +kubebuilder:validation:XValidation:rule="!(has(self.providers) && has(self.overrideConfig))",message="providers and overrideConfig are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.resources) && has(self.overrideConfig))",message="resources and overrideConfig are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.storage) && has(self.overrideConfig))",message="storage and overrideConfig are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.disabled) && has(self.overrideConfig))",message="disabled and overrideConfig are mutually exclusive" +type LlamaStackDistributionSpec struct { + // Distribution identifies the LlamaStack distribution image to deploy. + Distribution DistributionSpec `json:"distribution"` + + // Providers configures LlamaStack providers (inference, safety, vectorIo, toolRuntime, telemetry). + // Mutually exclusive with overrideConfig. + // +optional + Providers *ProvidersSpec `json:"providers,omitempty"` + + // Resources declares models, tools, and shields to register on startup. + // Mutually exclusive with overrideConfig. + // +optional + Resources *ResourcesSpec `json:"resources,omitempty"` + + // Storage configures state storage backends (kv and sql). + // Mutually exclusive with overrideConfig. + // +optional + Storage *StateStorageSpec `json:"storage,omitempty"` + + // Disabled lists API names to disable (e.g., postTraining, eval). + // Mutually exclusive with overrideConfig. + // +optional + Disabled []string `json:"disabled,omitempty"` + + // Networking configures network settings (port, TLS, expose, allowedFrom). + // +optional + Networking *NetworkingSpec `json:"networking,omitempty"` + + // Workload configures Kubernetes Deployment settings. + // +optional + Workload *WorkloadSpec `json:"workload,omitempty"` + + // ExternalProviders configures external provider injection (from spec 001). + // +optional + ExternalProviders *ExternalProvidersSpec `json:"externalProviders,omitempty"` + + // OverrideConfig provides a user-supplied ConfigMap as config.yaml. + // Mutually exclusive with providers, resources, storage, and disabled. + // +optional + OverrideConfig *OverrideConfigSpec `json:"overrideConfig,omitempty"` +} + +// --- Distribution --- + +// DistributionSpec identifies the LlamaStack distribution image to deploy. +// +kubebuilder:validation:XValidation:rule="!(has(self.name) && has(self.image))",message="only one of name or image can be specified" +// +kubebuilder:validation:XValidation:rule="has(self.name) || has(self.image)",message="one of name or image must be specified" +type DistributionSpec struct { + // Name is a distribution name mapped to an image via distributions.json. + // Mutually exclusive with image. + // +optional + Name string `json:"name,omitempty"` + + // Image is a direct container image reference. + // Mutually exclusive with name. + // +optional + Image string `json:"image,omitempty"` +} + +// --- Providers --- + +// ProvidersSpec configures LlamaStack providers by API type. +// Each field is a list of ProviderConfig; a single provider uses a one-element list. +type ProvidersSpec struct { + // Inference providers (e.g., vllm, ollama). + // +optional + // +kubebuilder:validation:XValidation:rule="self.size() <= 1 || self.all(p, has(p.id))",message="each provider must have an explicit id when multiple providers are specified" + Inference []ProviderConfig `json:"inference,omitempty"` + + // Safety providers (e.g., llama-guard). + // +optional + // +kubebuilder:validation:XValidation:rule="self.size() <= 1 || self.all(p, has(p.id))",message="each provider must have an explicit id when multiple providers are specified" + Safety []ProviderConfig `json:"safety,omitempty"` + + // VectorIo providers (e.g., pgvector, chromadb). + // +optional + // +kubebuilder:validation:XValidation:rule="self.size() <= 1 || self.all(p, has(p.id))",message="each provider must have an explicit id when multiple providers are specified" + VectorIo []ProviderConfig `json:"vectorIo,omitempty"` + + // ToolRuntime providers (e.g., brave-search, rag-runtime). + // +optional + // +kubebuilder:validation:XValidation:rule="self.size() <= 1 || self.all(p, has(p.id))",message="each provider must have an explicit id when multiple providers are specified" + ToolRuntime []ProviderConfig `json:"toolRuntime,omitempty"` + + // Telemetry providers (e.g., opentelemetry). + // +optional + // +kubebuilder:validation:XValidation:rule="self.size() <= 1 || self.all(p, has(p.id))",message="each provider must have an explicit id when multiple providers are specified" + Telemetry []ProviderConfig `json:"telemetry,omitempty"` +} + +// ProviderConfig defines a single LlamaStack provider instance. +type ProviderConfig struct { + // ID is a unique provider identifier. Required when multiple providers are + // specified for the same API type. Auto-generated from provider field for + // single-element lists. + // +optional + ID string `json:"id,omitempty"` + + // Provider is the provider type (e.g., vllm, llama-guard, pgvector). + // Maps to provider_type with "remote::" prefix in config.yaml. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Provider string `json:"provider"` + + // Endpoint is the provider endpoint URL. Maps to config.url in config.yaml. + // +optional + Endpoint string `json:"endpoint,omitempty"` + + // APIKey is a secret reference for API authentication. + // Resolved to env var LLSD__API_KEY. + // +optional + APIKey *SecretKeyRef `json:"apiKey,omitempty"` + + // SecretRefs are named secret references for provider-specific connection fields. + // Each key becomes the env var field suffix: LLSD__. + // +optional + SecretRefs map[string]SecretKeyRef `json:"secretRefs,omitempty"` + + // Settings is an escape hatch for provider-specific configuration. + // Merged into the provider's config section in config.yaml. + // No secret resolution is performed on settings values. + // +optional + Settings *apiextensionsv1.JSON `json:"settings,omitempty"` +} + +// SecretKeyRef references a specific key in a Kubernetes Secret. +type SecretKeyRef struct { + // Name is the name of the Secret. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Key is the key within the Secret. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// --- Resources --- + +// ResourcesSpec declares resources to register on startup. +type ResourcesSpec struct { + // Models to register with inference providers. + // +optional + Models []ModelConfig `json:"models,omitempty"` + + // Tools are tool group names to register with the toolRuntime provider. + // +optional + Tools []string `json:"tools,omitempty"` + + // Shields are safety shield names to register with the safety provider. + // +optional + Shields []string `json:"shields,omitempty"` +} + +// ModelConfig defines a model to register. +type ModelConfig struct { + // Name is the model identifier (e.g., llama3.2-8b). + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Provider is the provider ID to register this model with. + // Defaults to the first inference provider when omitted. + // +optional + Provider string `json:"provider,omitempty"` + + // ContextLength is the model context window size. + // +optional + ContextLength *int `json:"contextLength,omitempty"` + + // ModelType is the model type classification. + // +optional + ModelType string `json:"modelType,omitempty"` + + // Quantization is the quantization method used. + // +optional + Quantization string `json:"quantization,omitempty"` +} + +// --- Storage --- + +// StateStorageSpec configures state storage backends. +type StateStorageSpec struct { + // KV configures key-value storage (sqlite or redis). + // +optional + KV *KVStorageSpec `json:"kv,omitempty"` + + // SQL configures relational storage (sqlite or postgres). + // +optional + SQL *SQLStorageSpec `json:"sql,omitempty"` +} + +// KVStorageSpec configures key-value storage. +// +kubebuilder:validation:XValidation:rule="!has(self.type) || self.type != 'redis' || has(self.endpoint)",message="endpoint is required when type is redis" +type KVStorageSpec struct { + // Type is the storage backend type. + // +optional + // +kubebuilder:default:="sqlite" + // +kubebuilder:validation:Enum=sqlite;redis + Type string `json:"type,omitempty"` + + // Endpoint is the Redis endpoint URL. Required when type is redis. + // +optional + Endpoint string `json:"endpoint,omitempty"` + + // Password is a secret reference for Redis authentication. + // +optional + Password *SecretKeyRef `json:"password,omitempty"` +} + +// SQLStorageSpec configures relational storage. +// +kubebuilder:validation:XValidation:rule="!has(self.type) || self.type != 'postgres' || has(self.connectionString)",message="connectionString is required when type is postgres" +type SQLStorageSpec struct { + // Type is the storage backend type. + // +optional + // +kubebuilder:default:="sqlite" + // +kubebuilder:validation:Enum=sqlite;postgres + Type string `json:"type,omitempty"` + + // ConnectionString is a secret reference for the database connection string. + // Required when type is postgres. + // +optional + ConnectionString *SecretKeyRef `json:"connectionString,omitempty"` +} + +// --- Networking --- + +// NetworkingSpec configures network settings for the LlamaStack service. +type NetworkingSpec struct { + // Port is the server listen port. + // +optional + // +kubebuilder:default:=8321 + Port int32 `json:"port,omitempty"` + + // TLS configures TLS for the server. + // +optional + TLS *TLSSpec `json:"tls,omitempty"` + + // Expose controls external service exposure via Ingress/Route. + // +optional + Expose *ExposeConfig `json:"expose,omitempty"` + + // AllowedFrom configures namespace-based access control via NetworkPolicy. + // +optional + AllowedFrom *AllowedFromSpec `json:"allowedFrom,omitempty"` +} + +// TLSSpec configures TLS for the server. +// +kubebuilder:validation:XValidation:rule="!self.enabled || has(self.secretName)",message="secretName is required when TLS is enabled" +type TLSSpec struct { + // Enabled enables TLS on the server. + // +optional + Enabled bool `json:"enabled,omitempty"` + + // SecretName references a Kubernetes TLS Secret. Required when enabled is true. + // +optional + SecretName string `json:"secretName,omitempty"` + + // CABundle configures custom CA certificates. + // +optional + CABundle *CABundleConfig `json:"caBundle,omitempty"` +} + +// CABundleConfig references a ConfigMap containing CA certificates. +type CABundleConfig struct { + // ConfigMapName is the name of the ConfigMap containing CA bundle certificates. + // +kubebuilder:validation:Required + ConfigMapName string `json:"configMapName"` + + // ConfigMapNamespace is the namespace of the ConfigMap. + // Defaults to the same namespace as the CR. + // +optional + ConfigMapNamespace string `json:"configMapNamespace,omitempty"` + + // ConfigMapKeys specifies keys within the ConfigMap containing CA bundle data. + // Defaults to ["ca-bundle.crt"]. + // +optional + // +kubebuilder:validation:MaxItems=50 + ConfigMapKeys []string `json:"configMapKeys,omitempty"` +} + +// ExposeConfig controls external service exposure via Ingress/Route. +type ExposeConfig struct { + // Enabled enables external access via Ingress/Route. + // +optional + Enabled *bool `json:"enabled,omitempty"` + + // Hostname sets a custom hostname for the Ingress/Route. + // When omitted, an auto-generated hostname is used. + // +optional + Hostname string `json:"hostname,omitempty"` +} + +// AllowedFromSpec defines namespace-based access controls for NetworkPolicies. +type AllowedFromSpec struct { + // Namespaces is an explicit list of namespace names allowed to access the service. + // +optional + Namespaces []string `json:"namespaces,omitempty"` + + // Labels is a list of namespace label keys that grant access (OR semantics). + // +optional + Labels []string `json:"labels,omitempty"` +} + +// --- Workload --- + +// WorkloadSpec configures Kubernetes Deployment settings. +type WorkloadSpec struct { + // Replicas is the number of Pod replicas. + // +optional + // +kubebuilder:default:=1 + Replicas *int32 `json:"replicas,omitempty"` + + // Workers configures the number of uvicorn worker processes. + // +optional + // +kubebuilder:validation:Minimum=1 + Workers *int32 `json:"workers,omitempty"` + + // Resources configures CPU/memory requests and limits. + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + + // Autoscaling configures HorizontalPodAutoscaler. + // +optional + Autoscaling *AutoscalingSpec `json:"autoscaling,omitempty"` + + // Storage configures PVC for persistent data. + // +optional + Storage *PVCStorageSpec `json:"storage,omitempty"` + + // PodDisruptionBudget controls voluntary disruption tolerance. + // +optional + PodDisruptionBudget *PodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` + + // TopologySpreadConstraints defines Pod spreading rules. + // +optional + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + + // Overrides provides low-level Pod customization. + // +optional + Overrides *WorkloadOverrides `json:"overrides,omitempty"` +} + +// AutoscalingSpec configures HorizontalPodAutoscaler targets. +type AutoscalingSpec struct { + // MinReplicas is the lower bound replica count. + // +optional + MinReplicas *int32 `json:"minReplicas,omitempty"` + + // MaxReplicas is the upper bound replica count. + // +kubebuilder:validation:Required + MaxReplicas int32 `json:"maxReplicas"` + + // TargetCPUUtilizationPercentage configures CPU-based scaling. + // +optional + TargetCPUUtilizationPercentage *int32 `json:"targetCPUUtilizationPercentage,omitempty"` + + // TargetMemoryUtilizationPercentage configures memory-based scaling. + // +optional + TargetMemoryUtilizationPercentage *int32 `json:"targetMemoryUtilizationPercentage,omitempty"` +} + +// PVCStorageSpec configures persistent volume storage. +type PVCStorageSpec struct { + // Size is the PVC size (e.g., "10Gi"). + // +optional + Size *resource.Quantity `json:"size,omitempty"` + + // MountPath is where storage is mounted in the container. + // +optional + // +kubebuilder:default:="/.llama" + MountPath string `json:"mountPath,omitempty"` +} + +// PodDisruptionBudgetSpec defines voluntary disruption controls. +type PodDisruptionBudgetSpec struct { + // MinAvailable is the minimum number of pods that must remain available. + // +optional + MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"` + + // MaxUnavailable is the maximum number of pods that can be disrupted. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` +} + +// WorkloadOverrides provides low-level Pod customization. +type WorkloadOverrides struct { + // ServiceAccountName overrides the ServiceAccount. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // Env adds environment variables to the container. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Command overrides the container entrypoint. + // +optional + Command []string `json:"command,omitempty"` + + // Args overrides the container arguments. + // +optional + Args []string `json:"args,omitempty"` + + // Volumes adds volumes to the Pod. + // +optional + Volumes []corev1.Volume `json:"volumes,omitempty"` + + // VolumeMounts adds volume mounts to the container. + // +optional + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` +} + +// --- External Providers (spec 001) --- + +// ExternalProvidersSpec configures external provider injection. +type ExternalProvidersSpec struct { + // Inference external providers. + // +optional + Inference []ExternalProviderConfig `json:"inference,omitempty"` +} + +// ExternalProviderConfig defines an external provider sidecar. +type ExternalProviderConfig struct { + // ProviderID is the unique identifier for this provider. + // +kubebuilder:validation:Required + ProviderID string `json:"providerId"` + + // Image is the container image for the provider sidecar. + // +kubebuilder:validation:Required + Image string `json:"image"` +} + +// --- Override Config --- + +// OverrideConfigSpec provides a user-supplied ConfigMap as config.yaml. +type OverrideConfigSpec struct { + // ConfigMapName is the name of the ConfigMap containing config.yaml. + // Must reside in the same namespace as the CR. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + ConfigMapName string `json:"configMapName"` +} + +// --- Status --- + +// ResolvedDistributionStatus tracks the resolved distribution image. +type ResolvedDistributionStatus struct { + // Image is the resolved container image reference. + Image string `json:"image,omitempty"` + + // ConfigSource is the origin of the base config: "embedded" or "oci-label". + ConfigSource string `json:"configSource,omitempty"` + + // ConfigHash is the SHA-256 hash of the base config used. + ConfigHash string `json:"configHash,omitempty"` +} + +// ConfigGenerationStatus tracks config generation details. +type ConfigGenerationStatus struct { + // ConfigMapName is the name of the generated ConfigMap. + ConfigMapName string `json:"configMapName,omitempty"` + + // GeneratedAt is when the config was last generated. + GeneratedAt *metav1.Time `json:"generatedAt,omitempty"` + + // ProviderCount is the number of configured providers. + ProviderCount int `json:"providerCount,omitempty"` + + // ResourceCount is the number of registered resources. + ResourceCount int `json:"resourceCount,omitempty"` + + // ConfigVersion is the config.yaml schema version. + ConfigVersion int `json:"configVersion,omitempty"` +} + +// VersionInfo contains version-related information. +type VersionInfo struct { + // OperatorVersion is the version of the operator. + OperatorVersion string `json:"operatorVersion,omitempty"` + + // LlamaStackServerVersion is the version of the LlamaStack server. + LlamaStackServerVersion string `json:"llamaStackServerVersion,omitempty"` + + // LastUpdated is when the version information was last updated. + LastUpdated metav1.Time `json:"lastUpdated,omitempty"` +} + +// ProviderHealthStatus represents the health status of a provider. +type ProviderHealthStatus struct { + Status string `json:"status"` + Message string `json:"message"` +} + +// ProviderInfo represents a single provider from the providers endpoint. +type ProviderInfo struct { + API string `json:"api"` + ProviderID string `json:"provider_id"` + ProviderType string `json:"provider_type"` + Config apiextensionsv1.JSON `json:"config"` + Health ProviderHealthStatus `json:"health"` +} + +// DistributionConfig contains configuration from the providers endpoint. +type DistributionConfig struct { + ActiveDistribution string `json:"activeDistribution,omitempty"` + Providers []ProviderInfo `json:"providers,omitempty"` + AvailableDistributions map[string]string `json:"availableDistributions,omitempty"` +} + +// LlamaStackDistributionStatus defines the observed state of LlamaStackDistribution. +type LlamaStackDistributionStatus struct { + // Phase is the current phase of the distribution. + Phase DistributionPhase `json:"phase,omitempty"` + + // Version contains version information. + Version VersionInfo `json:"version,omitempty"` + + // DistributionConfig contains provider configuration from the server. + DistributionConfig DistributionConfig `json:"distributionConfig,omitempty"` + + // Conditions represent the latest available observations. + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ResolvedDistribution tracks the resolved image and config source. + ResolvedDistribution *ResolvedDistributionStatus `json:"resolvedDistribution,omitempty"` + + // ConfigGeneration tracks config generation details. + ConfigGeneration *ConfigGenerationStatus `json:"configGeneration,omitempty"` + + // AvailableReplicas is the number of available replicas. + AvailableReplicas int32 `json:"availableReplicas,omitempty"` + + // ServiceURL is the internal Kubernetes service URL. + ServiceURL string `json:"serviceURL,omitempty"` + + // RouteURL is the external URL (when expose is enabled). + // +optional + RouteURL *string `json:"routeURL,omitempty"` +} + +// --- Root types --- + +//+kubebuilder:object:root=true +//+kubebuilder:resource:shortName=llsd +//+kubebuilder:subresource:status +//+kubebuilder:storageversion +//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Distribution",type="string",JSONPath=".status.resolvedDistribution.image",priority=1 +//+kubebuilder:printcolumn:name="Config",type="string",JSONPath=".status.configGeneration.configMapName",priority=1 +//+kubebuilder:printcolumn:name="Providers",type="integer",JSONPath=".status.configGeneration.providerCount" +//+kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// LlamaStackDistribution is the Schema for the llamastackdistributions API. +type LlamaStackDistribution struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LlamaStackDistributionSpec `json:"spec"` + Status LlamaStackDistributionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// LlamaStackDistributionList contains a list of LlamaStackDistribution. +type LlamaStackDistributionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []LlamaStackDistribution `json:"items"` +} + +func init() { //nolint:gochecknoinits + SchemeBuilder.Register(&LlamaStackDistribution{}, &LlamaStackDistributionList{}) +} diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 000000000..45b4d942b --- /dev/null +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,866 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AllowedFromSpec) DeepCopyInto(out *AllowedFromSpec) { + *out = *in + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllowedFromSpec. +func (in *AllowedFromSpec) DeepCopy() *AllowedFromSpec { + if in == nil { + return nil + } + out := new(AllowedFromSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoscalingSpec) DeepCopyInto(out *AutoscalingSpec) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.TargetCPUUtilizationPercentage != nil { + in, out := &in.TargetCPUUtilizationPercentage, &out.TargetCPUUtilizationPercentage + *out = new(int32) + **out = **in + } + if in.TargetMemoryUtilizationPercentage != nil { + in, out := &in.TargetMemoryUtilizationPercentage, &out.TargetMemoryUtilizationPercentage + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingSpec. +func (in *AutoscalingSpec) DeepCopy() *AutoscalingSpec { + if in == nil { + return nil + } + out := new(AutoscalingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CABundleConfig) DeepCopyInto(out *CABundleConfig) { + *out = *in + if in.ConfigMapKeys != nil { + in, out := &in.ConfigMapKeys, &out.ConfigMapKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CABundleConfig. +func (in *CABundleConfig) DeepCopy() *CABundleConfig { + if in == nil { + return nil + } + out := new(CABundleConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigGenerationStatus) DeepCopyInto(out *ConfigGenerationStatus) { + *out = *in + if in.GeneratedAt != nil { + in, out := &in.GeneratedAt, &out.GeneratedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigGenerationStatus. +func (in *ConfigGenerationStatus) DeepCopy() *ConfigGenerationStatus { + if in == nil { + return nil + } + out := new(ConfigGenerationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DistributionConfig) DeepCopyInto(out *DistributionConfig) { + *out = *in + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]ProviderInfo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AvailableDistributions != nil { + in, out := &in.AvailableDistributions, &out.AvailableDistributions + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributionConfig. +func (in *DistributionConfig) DeepCopy() *DistributionConfig { + if in == nil { + return nil + } + out := new(DistributionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DistributionSpec) DeepCopyInto(out *DistributionSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributionSpec. +func (in *DistributionSpec) DeepCopy() *DistributionSpec { + if in == nil { + return nil + } + out := new(DistributionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExposeConfig) DeepCopyInto(out *ExposeConfig) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExposeConfig. +func (in *ExposeConfig) DeepCopy() *ExposeConfig { + if in == nil { + return nil + } + out := new(ExposeConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalProviderConfig) DeepCopyInto(out *ExternalProviderConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProviderConfig. +func (in *ExternalProviderConfig) DeepCopy() *ExternalProviderConfig { + if in == nil { + return nil + } + out := new(ExternalProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalProvidersSpec) DeepCopyInto(out *ExternalProvidersSpec) { + *out = *in + if in.Inference != nil { + in, out := &in.Inference, &out.Inference + *out = make([]ExternalProviderConfig, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProvidersSpec. +func (in *ExternalProvidersSpec) DeepCopy() *ExternalProvidersSpec { + if in == nil { + return nil + } + out := new(ExternalProvidersSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KVStorageSpec) DeepCopyInto(out *KVStorageSpec) { + *out = *in + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(SecretKeyRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KVStorageSpec. +func (in *KVStorageSpec) DeepCopy() *KVStorageSpec { + if in == nil { + return nil + } + out := new(KVStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LlamaStackDistribution) DeepCopyInto(out *LlamaStackDistribution) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaStackDistribution. +func (in *LlamaStackDistribution) DeepCopy() *LlamaStackDistribution { + if in == nil { + return nil + } + out := new(LlamaStackDistribution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LlamaStackDistribution) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LlamaStackDistributionList) DeepCopyInto(out *LlamaStackDistributionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LlamaStackDistribution, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaStackDistributionList. +func (in *LlamaStackDistributionList) DeepCopy() *LlamaStackDistributionList { + if in == nil { + return nil + } + out := new(LlamaStackDistributionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LlamaStackDistributionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LlamaStackDistributionSpec) DeepCopyInto(out *LlamaStackDistributionSpec) { + *out = *in + out.Distribution = in.Distribution + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = new(ProvidersSpec) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(ResourcesSpec) + (*in).DeepCopyInto(*out) + } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(StateStorageSpec) + (*in).DeepCopyInto(*out) + } + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Networking != nil { + in, out := &in.Networking, &out.Networking + *out = new(NetworkingSpec) + (*in).DeepCopyInto(*out) + } + if in.Workload != nil { + in, out := &in.Workload, &out.Workload + *out = new(WorkloadSpec) + (*in).DeepCopyInto(*out) + } + if in.ExternalProviders != nil { + in, out := &in.ExternalProviders, &out.ExternalProviders + *out = new(ExternalProvidersSpec) + (*in).DeepCopyInto(*out) + } + if in.OverrideConfig != nil { + in, out := &in.OverrideConfig, &out.OverrideConfig + *out = new(OverrideConfigSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaStackDistributionSpec. +func (in *LlamaStackDistributionSpec) DeepCopy() *LlamaStackDistributionSpec { + if in == nil { + return nil + } + out := new(LlamaStackDistributionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LlamaStackDistributionStatus) DeepCopyInto(out *LlamaStackDistributionStatus) { + *out = *in + in.Version.DeepCopyInto(&out.Version) + in.DistributionConfig.DeepCopyInto(&out.DistributionConfig) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ResolvedDistribution != nil { + in, out := &in.ResolvedDistribution, &out.ResolvedDistribution + *out = new(ResolvedDistributionStatus) + **out = **in + } + if in.ConfigGeneration != nil { + in, out := &in.ConfigGeneration, &out.ConfigGeneration + *out = new(ConfigGenerationStatus) + (*in).DeepCopyInto(*out) + } + if in.RouteURL != nil { + in, out := &in.RouteURL, &out.RouteURL + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaStackDistributionStatus. +func (in *LlamaStackDistributionStatus) DeepCopy() *LlamaStackDistributionStatus { + if in == nil { + return nil + } + out := new(LlamaStackDistributionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ModelConfig) DeepCopyInto(out *ModelConfig) { + *out = *in + if in.ContextLength != nil { + in, out := &in.ContextLength, &out.ContextLength + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModelConfig. +func (in *ModelConfig) DeepCopy() *ModelConfig { + if in == nil { + return nil + } + out := new(ModelConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkingSpec) DeepCopyInto(out *NetworkingSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + (*in).DeepCopyInto(*out) + } + if in.Expose != nil { + in, out := &in.Expose, &out.Expose + *out = new(ExposeConfig) + (*in).DeepCopyInto(*out) + } + if in.AllowedFrom != nil { + in, out := &in.AllowedFrom, &out.AllowedFrom + *out = new(AllowedFromSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingSpec. +func (in *NetworkingSpec) DeepCopy() *NetworkingSpec { + if in == nil { + return nil + } + out := new(NetworkingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OverrideConfigSpec) DeepCopyInto(out *OverrideConfigSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OverrideConfigSpec. +func (in *OverrideConfigSpec) DeepCopy() *OverrideConfigSpec { + if in == nil { + return nil + } + out := new(OverrideConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PVCStorageSpec) DeepCopyInto(out *PVCStorageSpec) { + *out = *in + if in.Size != nil { + in, out := &in.Size, &out.Size + x := (*in).DeepCopy() + *out = &x + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PVCStorageSpec. +func (in *PVCStorageSpec) DeepCopy() *PVCStorageSpec { + if in == nil { + return nil + } + out := new(PVCStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodDisruptionBudgetSpec) DeepCopyInto(out *PodDisruptionBudgetSpec) { + *out = *in + if in.MinAvailable != nil { + in, out := &in.MinAvailable, &out.MinAvailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodDisruptionBudgetSpec. +func (in *PodDisruptionBudgetSpec) DeepCopy() *PodDisruptionBudgetSpec { + if in == nil { + return nil + } + out := new(PodDisruptionBudgetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderConfig) DeepCopyInto(out *ProviderConfig) { + *out = *in + if in.APIKey != nil { + in, out := &in.APIKey, &out.APIKey + *out = new(SecretKeyRef) + **out = **in + } + if in.SecretRefs != nil { + in, out := &in.SecretRefs, &out.SecretRefs + *out = make(map[string]SecretKeyRef, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(v1.JSON) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfig. +func (in *ProviderConfig) DeepCopy() *ProviderConfig { + if in == nil { + return nil + } + out := new(ProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderHealthStatus) DeepCopyInto(out *ProviderHealthStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderHealthStatus. +func (in *ProviderHealthStatus) DeepCopy() *ProviderHealthStatus { + if in == nil { + return nil + } + out := new(ProviderHealthStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderInfo) DeepCopyInto(out *ProviderInfo) { + *out = *in + in.Config.DeepCopyInto(&out.Config) + out.Health = in.Health +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderInfo. +func (in *ProviderInfo) DeepCopy() *ProviderInfo { + if in == nil { + return nil + } + out := new(ProviderInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProvidersSpec) DeepCopyInto(out *ProvidersSpec) { + *out = *in + if in.Inference != nil { + in, out := &in.Inference, &out.Inference + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Safety != nil { + in, out := &in.Safety, &out.Safety + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VectorIo != nil { + in, out := &in.VectorIo, &out.VectorIo + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ToolRuntime != nil { + in, out := &in.ToolRuntime, &out.ToolRuntime + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Telemetry != nil { + in, out := &in.Telemetry, &out.Telemetry + *out = make([]ProviderConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvidersSpec. +func (in *ProvidersSpec) DeepCopy() *ProvidersSpec { + if in == nil { + return nil + } + out := new(ProvidersSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResolvedDistributionStatus) DeepCopyInto(out *ResolvedDistributionStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolvedDistributionStatus. +func (in *ResolvedDistributionStatus) DeepCopy() *ResolvedDistributionStatus { + if in == nil { + return nil + } + out := new(ResolvedDistributionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourcesSpec) DeepCopyInto(out *ResourcesSpec) { + *out = *in + if in.Models != nil { + in, out := &in.Models, &out.Models + *out = make([]ModelConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Tools != nil { + in, out := &in.Tools, &out.Tools + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Shields != nil { + in, out := &in.Shields, &out.Shields + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcesSpec. +func (in *ResourcesSpec) DeepCopy() *ResourcesSpec { + if in == nil { + return nil + } + out := new(ResourcesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SQLStorageSpec) DeepCopyInto(out *SQLStorageSpec) { + *out = *in + if in.ConnectionString != nil { + in, out := &in.ConnectionString, &out.ConnectionString + *out = new(SecretKeyRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SQLStorageSpec. +func (in *SQLStorageSpec) DeepCopy() *SQLStorageSpec { + if in == nil { + return nil + } + out := new(SQLStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef. +func (in *SecretKeyRef) DeepCopy() *SecretKeyRef { + if in == nil { + return nil + } + out := new(SecretKeyRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StateStorageSpec) DeepCopyInto(out *StateStorageSpec) { + *out = *in + if in.KV != nil { + in, out := &in.KV, &out.KV + *out = new(KVStorageSpec) + (*in).DeepCopyInto(*out) + } + if in.SQL != nil { + in, out := &in.SQL, &out.SQL + *out = new(SQLStorageSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StateStorageSpec. +func (in *StateStorageSpec) DeepCopy() *StateStorageSpec { + if in == nil { + return nil + } + out := new(StateStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(CABundleConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionInfo) DeepCopyInto(out *VersionInfo) { + *out = *in + in.LastUpdated.DeepCopyInto(&out.LastUpdated) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionInfo. +func (in *VersionInfo) DeepCopy() *VersionInfo { + if in == nil { + return nil + } + out := new(VersionInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkloadOverrides) DeepCopyInto(out *WorkloadOverrides) { + *out = *in + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadOverrides. +func (in *WorkloadOverrides) DeepCopy() *WorkloadOverrides { + if in == nil { + return nil + } + out := new(WorkloadOverrides) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkloadSpec) DeepCopyInto(out *WorkloadSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Workers != nil { + in, out := &in.Workers, &out.Workers + *out = new(int32) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(AutoscalingSpec) + (*in).DeepCopyInto(*out) + } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(PVCStorageSpec) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(PodDisruptionBudgetSpec) + (*in).DeepCopyInto(*out) + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Overrides != nil { + in, out := &in.Overrides, &out.Overrides + *out = new(WorkloadOverrides) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadSpec. +func (in *WorkloadSpec) DeepCopy() *WorkloadSpec { + if in == nil { + return nil + } + out := new(WorkloadSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/llamastack.io_llamastackdistributions.yaml b/config/crd/bases/llamastack.io_llamastackdistributions.yaml index 494e6a928..8977f9fbe 100644 --- a/config/crd/bases/llamastack.io_llamastackdistributions.yaml +++ b/config/crd/bases/llamastack.io_llamastackdistributions.yaml @@ -2781,6 +2781,3327 @@ spec: - jsonPath: .spec.server.tlsConfig.caBundle.configMapName - jsonPath: .spec.server.tlsConfig.caBundle.configMapNamespace served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.resolvedDistribution.image + name: Distribution + priority: 1 + type: string + - jsonPath: .status.configGeneration.configMapName + name: Config + priority: 1 + type: string + - jsonPath: .status.configGeneration.providerCount + name: Providers + type: integer + - jsonPath: .status.availableReplicas + name: Available + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: LlamaStackDistribution is the Schema for the llamastackdistributions + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: LlamaStackDistributionSpec defines the desired state of LlamaStackDistribution. + properties: + disabled: + description: |- + Disabled lists API names to disable (e.g., postTraining, eval). + Mutually exclusive with overrideConfig. + items: + type: string + type: array + distribution: + description: Distribution identifies the LlamaStack distribution image + to deploy. + properties: + image: + description: |- + Image is a direct container image reference. + Mutually exclusive with name. + type: string + name: + description: |- + Name is a distribution name mapped to an image via distributions.json. + Mutually exclusive with image. + type: string + type: object + x-kubernetes-validations: + - message: only one of name or image can be specified + rule: '!(has(self.name) && has(self.image))' + - message: one of name or image must be specified + rule: has(self.name) || has(self.image) + externalProviders: + description: ExternalProviders configures external provider injection + (from spec 001). + properties: + inference: + description: Inference external providers. + items: + description: ExternalProviderConfig defines an external provider + sidecar. + properties: + image: + description: Image is the container image for the provider + sidecar. + type: string + providerId: + description: ProviderID is the unique identifier for this + provider. + type: string + required: + - image + - providerId + type: object + type: array + type: object + networking: + description: Networking configures network settings (port, TLS, expose, + allowedFrom). + properties: + allowedFrom: + description: AllowedFrom configures namespace-based access control + via NetworkPolicy. + properties: + labels: + description: Labels is a list of namespace label keys that + grant access (OR semantics). + items: + type: string + type: array + namespaces: + description: Namespaces is an explicit list of namespace names + allowed to access the service. + items: + type: string + type: array + type: object + expose: + description: Expose controls external service exposure via Ingress/Route. + properties: + enabled: + description: Enabled enables external access via Ingress/Route. + type: boolean + hostname: + description: |- + Hostname sets a custom hostname for the Ingress/Route. + When omitted, an auto-generated hostname is used. + type: string + type: object + port: + default: 8321 + description: Port is the server listen port. + format: int32 + type: integer + tls: + description: TLS configures TLS for the server. + properties: + caBundle: + description: CABundle configures custom CA certificates. + properties: + configMapKeys: + description: |- + ConfigMapKeys specifies keys within the ConfigMap containing CA bundle data. + Defaults to ["ca-bundle.crt"]. + items: + type: string + maxItems: 50 + type: array + configMapName: + description: ConfigMapName is the name of the ConfigMap + containing CA bundle certificates. + type: string + configMapNamespace: + description: |- + ConfigMapNamespace is the namespace of the ConfigMap. + Defaults to the same namespace as the CR. + type: string + required: + - configMapName + type: object + enabled: + description: Enabled enables TLS on the server. + type: boolean + secretName: + description: SecretName references a Kubernetes TLS Secret. + Required when enabled is true. + type: string + type: object + x-kubernetes-validations: + - message: secretName is required when TLS is enabled + rule: '!self.enabled || has(self.secretName)' + type: object + overrideConfig: + description: |- + OverrideConfig provides a user-supplied ConfigMap as config.yaml. + Mutually exclusive with providers, resources, storage, and disabled. + properties: + configMapName: + description: |- + ConfigMapName is the name of the ConfigMap containing config.yaml. + Must reside in the same namespace as the CR. + minLength: 1 + type: string + required: + - configMapName + type: object + providers: + description: |- + Providers configures LlamaStack providers (inference, safety, vectorIo, toolRuntime, telemetry). + Mutually exclusive with overrideConfig. + properties: + inference: + description: Inference providers (e.g., vllm, ollama). + items: + description: ProviderConfig defines a single LlamaStack provider + instance. + properties: + apiKey: + description: |- + APIKey is a secret reference for API authentication. + Resolved to env var LLSD__API_KEY. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + endpoint: + description: Endpoint is the provider endpoint URL. Maps + to config.url in config.yaml. + type: string + id: + description: |- + ID is a unique provider identifier. Required when multiple providers are + specified for the same API type. Auto-generated from provider field for + single-element lists. + type: string + provider: + description: |- + Provider is the provider type (e.g., vllm, llama-guard, pgvector). + Maps to provider_type with "remote::" prefix in config.yaml. + minLength: 1 + type: string + secretRefs: + additionalProperties: + description: SecretKeyRef references a specific key in + a Kubernetes Secret. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + description: |- + SecretRefs are named secret references for provider-specific connection fields. + Each key becomes the env var field suffix: LLSD__. + type: object + settings: + description: |- + Settings is an escape hatch for provider-specific configuration. + Merged into the provider's config section in config.yaml. + No secret resolution is performed on settings values. + x-kubernetes-preserve-unknown-fields: true + required: + - provider + type: object + type: array + x-kubernetes-validations: + - message: each provider must have an explicit id when multiple + providers are specified + rule: self.size() <= 1 || self.all(p, has(p.id)) + safety: + description: Safety providers (e.g., llama-guard). + items: + description: ProviderConfig defines a single LlamaStack provider + instance. + properties: + apiKey: + description: |- + APIKey is a secret reference for API authentication. + Resolved to env var LLSD__API_KEY. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + endpoint: + description: Endpoint is the provider endpoint URL. Maps + to config.url in config.yaml. + type: string + id: + description: |- + ID is a unique provider identifier. Required when multiple providers are + specified for the same API type. Auto-generated from provider field for + single-element lists. + type: string + provider: + description: |- + Provider is the provider type (e.g., vllm, llama-guard, pgvector). + Maps to provider_type with "remote::" prefix in config.yaml. + minLength: 1 + type: string + secretRefs: + additionalProperties: + description: SecretKeyRef references a specific key in + a Kubernetes Secret. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + description: |- + SecretRefs are named secret references for provider-specific connection fields. + Each key becomes the env var field suffix: LLSD__. + type: object + settings: + description: |- + Settings is an escape hatch for provider-specific configuration. + Merged into the provider's config section in config.yaml. + No secret resolution is performed on settings values. + x-kubernetes-preserve-unknown-fields: true + required: + - provider + type: object + type: array + x-kubernetes-validations: + - message: each provider must have an explicit id when multiple + providers are specified + rule: self.size() <= 1 || self.all(p, has(p.id)) + telemetry: + description: Telemetry providers (e.g., opentelemetry). + items: + description: ProviderConfig defines a single LlamaStack provider + instance. + properties: + apiKey: + description: |- + APIKey is a secret reference for API authentication. + Resolved to env var LLSD__API_KEY. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + endpoint: + description: Endpoint is the provider endpoint URL. Maps + to config.url in config.yaml. + type: string + id: + description: |- + ID is a unique provider identifier. Required when multiple providers are + specified for the same API type. Auto-generated from provider field for + single-element lists. + type: string + provider: + description: |- + Provider is the provider type (e.g., vllm, llama-guard, pgvector). + Maps to provider_type with "remote::" prefix in config.yaml. + minLength: 1 + type: string + secretRefs: + additionalProperties: + description: SecretKeyRef references a specific key in + a Kubernetes Secret. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + description: |- + SecretRefs are named secret references for provider-specific connection fields. + Each key becomes the env var field suffix: LLSD__. + type: object + settings: + description: |- + Settings is an escape hatch for provider-specific configuration. + Merged into the provider's config section in config.yaml. + No secret resolution is performed on settings values. + x-kubernetes-preserve-unknown-fields: true + required: + - provider + type: object + type: array + x-kubernetes-validations: + - message: each provider must have an explicit id when multiple + providers are specified + rule: self.size() <= 1 || self.all(p, has(p.id)) + toolRuntime: + description: ToolRuntime providers (e.g., brave-search, rag-runtime). + items: + description: ProviderConfig defines a single LlamaStack provider + instance. + properties: + apiKey: + description: |- + APIKey is a secret reference for API authentication. + Resolved to env var LLSD__API_KEY. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + endpoint: + description: Endpoint is the provider endpoint URL. Maps + to config.url in config.yaml. + type: string + id: + description: |- + ID is a unique provider identifier. Required when multiple providers are + specified for the same API type. Auto-generated from provider field for + single-element lists. + type: string + provider: + description: |- + Provider is the provider type (e.g., vllm, llama-guard, pgvector). + Maps to provider_type with "remote::" prefix in config.yaml. + minLength: 1 + type: string + secretRefs: + additionalProperties: + description: SecretKeyRef references a specific key in + a Kubernetes Secret. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + description: |- + SecretRefs are named secret references for provider-specific connection fields. + Each key becomes the env var field suffix: LLSD__. + type: object + settings: + description: |- + Settings is an escape hatch for provider-specific configuration. + Merged into the provider's config section in config.yaml. + No secret resolution is performed on settings values. + x-kubernetes-preserve-unknown-fields: true + required: + - provider + type: object + type: array + x-kubernetes-validations: + - message: each provider must have an explicit id when multiple + providers are specified + rule: self.size() <= 1 || self.all(p, has(p.id)) + vectorIo: + description: VectorIo providers (e.g., pgvector, chromadb). + items: + description: ProviderConfig defines a single LlamaStack provider + instance. + properties: + apiKey: + description: |- + APIKey is a secret reference for API authentication. + Resolved to env var LLSD__API_KEY. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + endpoint: + description: Endpoint is the provider endpoint URL. Maps + to config.url in config.yaml. + type: string + id: + description: |- + ID is a unique provider identifier. Required when multiple providers are + specified for the same API type. Auto-generated from provider field for + single-element lists. + type: string + provider: + description: |- + Provider is the provider type (e.g., vllm, llama-guard, pgvector). + Maps to provider_type with "remote::" prefix in config.yaml. + minLength: 1 + type: string + secretRefs: + additionalProperties: + description: SecretKeyRef references a specific key in + a Kubernetes Secret. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + description: |- + SecretRefs are named secret references for provider-specific connection fields. + Each key becomes the env var field suffix: LLSD__. + type: object + settings: + description: |- + Settings is an escape hatch for provider-specific configuration. + Merged into the provider's config section in config.yaml. + No secret resolution is performed on settings values. + x-kubernetes-preserve-unknown-fields: true + required: + - provider + type: object + type: array + x-kubernetes-validations: + - message: each provider must have an explicit id when multiple + providers are specified + rule: self.size() <= 1 || self.all(p, has(p.id)) + type: object + resources: + description: |- + Resources declares models, tools, and shields to register on startup. + Mutually exclusive with overrideConfig. + properties: + models: + description: Models to register with inference providers. + items: + description: ModelConfig defines a model to register. + properties: + contextLength: + description: ContextLength is the model context window size. + type: integer + modelType: + description: ModelType is the model type classification. + type: string + name: + description: Name is the model identifier (e.g., llama3.2-8b). + minLength: 1 + type: string + provider: + description: |- + Provider is the provider ID to register this model with. + Defaults to the first inference provider when omitted. + type: string + quantization: + description: Quantization is the quantization method used. + type: string + required: + - name + type: object + type: array + shields: + description: Shields are safety shield names to register with + the safety provider. + items: + type: string + type: array + tools: + description: Tools are tool group names to register with the toolRuntime + provider. + items: + type: string + type: array + type: object + storage: + description: |- + Storage configures state storage backends (kv and sql). + Mutually exclusive with overrideConfig. + properties: + kv: + description: KV configures key-value storage (sqlite or redis). + properties: + endpoint: + description: Endpoint is the Redis endpoint URL. Required + when type is redis. + type: string + password: + description: Password is a secret reference for Redis authentication. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + type: + default: sqlite + description: Type is the storage backend type. + enum: + - sqlite + - redis + type: string + type: object + x-kubernetes-validations: + - message: endpoint is required when type is redis + rule: '!has(self.type) || self.type != ''redis'' || has(self.endpoint)' + sql: + description: SQL configures relational storage (sqlite or postgres). + properties: + connectionString: + description: |- + ConnectionString is a secret reference for the database connection string. + Required when type is postgres. + properties: + key: + description: Key is the key within the Secret. + minLength: 1 + type: string + name: + description: Name is the name of the Secret. + minLength: 1 + type: string + required: + - key + - name + type: object + type: + default: sqlite + description: Type is the storage backend type. + enum: + - sqlite + - postgres + type: string + type: object + x-kubernetes-validations: + - message: connectionString is required when type is postgres + rule: '!has(self.type) || self.type != ''postgres'' || has(self.connectionString)' + type: object + workload: + description: Workload configures Kubernetes Deployment settings. + properties: + autoscaling: + description: Autoscaling configures HorizontalPodAutoscaler. + properties: + maxReplicas: + description: MaxReplicas is the upper bound replica count. + format: int32 + type: integer + minReplicas: + description: MinReplicas is the lower bound replica count. + format: int32 + type: integer + targetCPUUtilizationPercentage: + description: TargetCPUUtilizationPercentage configures CPU-based + scaling. + format: int32 + type: integer + targetMemoryUtilizationPercentage: + description: TargetMemoryUtilizationPercentage configures + memory-based scaling. + format: int32 + type: integer + required: + - maxReplicas + type: object + overrides: + description: Overrides provides low-level Pod customization. + properties: + args: + description: Args overrides the container arguments. + items: + type: string + type: array + command: + description: Command overrides the container entrypoint. + items: + type: string + type: array + env: + description: Env adds environment variables to the container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + serviceAccountName: + description: ServiceAccountName overrides the ServiceAccount. + type: string + volumeMounts: + description: VolumeMounts adds volume mounts to the container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + description: Volumes adds volumes to the Pod. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + Deprecated: AWSElasticBlockStore is deprecated. All operations for the in-tree + awsElasticBlockStore type are redirected to the ebs.csi.aws.com CSI driver. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: |- + azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod. + Deprecated: AzureDisk is deprecated. All operations for the in-tree azureDisk type + are redirected to the disk.csi.azure.com CSI driver. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: |- + azureFile represents an Azure File Service mount on the host and bind mount to the pod. + Deprecated: AzureFile is deprecated. All operations for the in-tree azureFile type + are redirected to the file.csi.azure.com CSI driver. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: |- + cephFS represents a Ceph FS mount on the host that shares a pod's lifetime. + Deprecated: CephFS is deprecated and the in-tree cephfs type is no longer supported. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. All operations for the in-tree cinder type + are redirected to the cinder.csi.openstack.org CSI driver. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name, + namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `-` where + `` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string or nil value indicates that no + VolumeAttributesClass will be applied to the claim. If the claim enters an Infeasible error state, + this field can be reset to its previous value (including nil) to cancel the modification. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + Deprecated: FlexVolume is deprecated. Consider using a CSIDriver instead. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: |- + flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running. + Deprecated: Flocker is deprecated and the in-tree flocker type is no longer supported. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + Deprecated: GCEPersistentDisk is deprecated. All operations for the in-tree + gcePersistentDisk type are redirected to the pd.csi.storage.gke.io CSI driver. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + Deprecated: Glusterfs is deprecated and the in-tree glusterfs type is no longer supported. + properties: + endpoints: + description: endpoints is the endpoint name that + details Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: |- + image represents an OCI object (a container image or artifact) pulled and mounted on the kubelet's host machine. + The volume is resolved at pod startup depending on which PullPolicy value is provided: + + - Always: the kubelet always attempts to pull the reference. Container creation will fail If the pull fails. + - Never: the kubelet never pulls the reference and only uses a local image or artifact. Container creation will fail if the reference isn't present. + - IfNotPresent: the kubelet pulls if the reference isn't already present on disk. Container creation will fail if the reference isn't present and the pull fails. + + The volume gets re-resolved if the pod gets deleted and recreated, which means that new remote content will become available on pod recreation. + A failure to resolve or pull the image during pod startup will block containers from starting and may add significant latency. Failures will be retried using normal volume backoff and will be reported on the pod reason and message. + The types of objects that may be mounted by this volume are defined by the container runtime implementation on a host machine and at minimum must include all valid types supported by the container image field. + The OCI object gets mounted in a single directory (spec.containers[*].volumeMounts.mountPath) by merging the manifest layers in the same way as for container images. + The volume will be mounted read-only (ro) and non-executable files (noexec). + Sub path mounts for containers are not supported (spec.containers[*].volumeMounts.subpath) before 1.33. + The field spec.securityContext.fsGroupChangePolicy has no effect on this volume type. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. Container creation will fail If the pull fails. + Never: the kubelet never pulls the reference and only uses a local image or artifact. Container creation will fail if the reference isn't present. + IfNotPresent: the kubelet pulls if the reference isn't already present on disk. Container creation will fail if the reference isn't present and the pull fails. + Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + Pull secrets will be assembled in the same way as for the container image by looking up node credentials, SA image pull secrets, and pod spec image pull secrets. + More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config management to default or override + container images in workload controllers like Deployments and StatefulSets. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes/#iscsi + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: |- + photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine. + Deprecated: PhotonPersistentDisk is deprecated and the in-tree photonPersistentDisk type is no longer supported. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: |- + portworxVolume represents a portworx volume attached and mounted on kubelets host machine. + Deprecated: PortworxVolume is deprecated. All operations for the in-tree portworxVolume type + are redirected to the pxd.portworx.com CSI driver when the CSIMigrationPortworx feature-gate + is on. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name, namespace and uid + are supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS client or server. + + Kubelet generates a private key and uses it to send a + PodCertificateRequest to the named signer. Once the signer approves the + request and issues a certificate chain, Kubelet writes the key and + certificate chain to the pod filesystem. The pod does not start until + certificates have been issued for each podCertificate projected volume + source in its spec. + + Kubelet will begin trying to rotate the certificate at the time indicated + by the signer using the PodCertificateRequest.Status.BeginRefreshAt + timestamp. + + Kubelet can write a single file, indicated by the credentialBundlePath + field, or separate files, indicated by the keyPath and + certificateChainPath fields. + + The credential bundle is a single file in PEM format. The first PEM + entry is the private key (in PKCS#8 format), and the remaining PEM + entries are the certificate chain issued by the signer (typically, + signers will return their certificate chain in leaf-to-root order). + + Prefer using the credential bundle format, since your application code + can read it atomically. If you use keyPath and certificateChainPath, + your application must make two separate file reads. If these coincide + with a certificate rotation, it is possible that the private key and leaf + certificate you read may not correspond to each other. Your application + will need to check for this condition, and re-read until they are + consistent. + + The named signer controls chooses the format of the certificate it + issues; consult the signer implementation's documentation to learn how to + use the certificates it issues. + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. When using keyPath + and certificateChainPath, your application needs to check that the key + and leaf certificate are consistent, because it is possible to read the + files mid-rotation. + type: string + credentialBundlePath: + description: |- + Write the credential bundle at this path in the projected volume. + + The credential bundle is a single file that contains multiple PEM blocks. + The first PEM block is a PRIVATE KEY block, containing a PKCS#8 private + key. + + The remaining blocks are CERTIFICATE blocks, containing the issued + certificate chain from the signer (leaf and any intermediates). + + Using credentialBundlePath lets your Pod's application code make a single + atomic read that retrieves a consistent key and certificate chain. If you + project them to separate files, your application code will need to + additionally check that the leaf certificate was issued to the key. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. When using keyPath + and certificateChainPath, your application needs to check that the key + and leaf certificate are consistent, because it is possible to read the + files mid-rotation. + type: string + keyType: + description: |- + The type of keypair Kubelet will generate for the pod. + + Valid values are "RSA3072", "RSA4096", "ECDSAP256", "ECDSAP384", + "ECDSAP521", and "ED25519". + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + + Kubelet copies this value verbatim into the PodCertificateRequests it + generates for this projection. + + If omitted, kube-apiserver will set it to 86400(24 hours). kube-apiserver + will reject values shorter than 3600 (1 hour). The maximum allowable + value is 7862400 (91 days). + + The signer implementation is then free to issue a certificate with any + lifetime *shorter* than MaxExpirationSeconds, but no shorter than 3600 + seconds (1 hour). This constraint is enforced by kube-apiserver. + `kubernetes.io` signers will never issue certificates with a lifetime + longer than 24 hours. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + required: + - keyType + - signerName + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: |- + quobyte represents a Quobyte mount on the host that shares a pod's lifetime. + Deprecated: Quobyte is deprecated and the in-tree quobyte type is no longer supported. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + Deprecated: RBD is deprecated and the in-tree rbd type is no longer supported. + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: |- + scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes. + Deprecated: ScaleIO is deprecated and the in-tree scaleIO type is no longer supported. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: |- + storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes. + Deprecated: StorageOS is deprecated and the in-tree storageos type is no longer supported. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: |- + vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine. + Deprecated: VsphereVolume is deprecated. All operations for the in-tree vsphereVolume type + are redirected to the csi.vsphere.vmware.com CSI driver. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + podDisruptionBudget: + description: PodDisruptionBudget controls voluntary disruption + tolerance. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable is the maximum number of pods + that can be disrupted. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable is the minimum number of pods that + must remain available. + x-kubernetes-int-or-string: true + type: object + replicas: + default: 1 + description: Replicas is the number of Pod replicas. + format: int32 + type: integer + resources: + description: Resources configures CPU/memory requests and limits. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + storage: + description: Storage configures PVC for persistent data. + properties: + mountPath: + default: /.llama + description: MountPath is where storage is mounted in the + container. + type: string + size: + anyOf: + - type: integer + - type: string + description: Size is the PVC size (e.g., "10Gi"). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines Pod spreading rules. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + workers: + description: Workers configures the number of uvicorn worker processes. + format: int32 + minimum: 1 + type: integer + type: object + required: + - distribution + type: object + x-kubernetes-validations: + - message: providers and overrideConfig are mutually exclusive + rule: '!(has(self.providers) && has(self.overrideConfig))' + - message: resources and overrideConfig are mutually exclusive + rule: '!(has(self.resources) && has(self.overrideConfig))' + - message: storage and overrideConfig are mutually exclusive + rule: '!(has(self.storage) && has(self.overrideConfig))' + - message: disabled and overrideConfig are mutually exclusive + rule: '!(has(self.disabled) && has(self.overrideConfig))' + status: + description: LlamaStackDistributionStatus defines the observed state of + LlamaStackDistribution. + properties: + availableReplicas: + description: AvailableReplicas is the number of available replicas. + format: int32 + type: integer + conditions: + description: Conditions represent the latest available observations. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + configGeneration: + description: ConfigGeneration tracks config generation details. + properties: + configMapName: + description: ConfigMapName is the name of the generated ConfigMap. + type: string + configVersion: + description: ConfigVersion is the config.yaml schema version. + type: integer + generatedAt: + description: GeneratedAt is when the config was last generated. + format: date-time + type: string + providerCount: + description: ProviderCount is the number of configured providers. + type: integer + resourceCount: + description: ResourceCount is the number of registered resources. + type: integer + type: object + distributionConfig: + description: DistributionConfig contains provider configuration from + the server. + properties: + activeDistribution: + type: string + availableDistributions: + additionalProperties: + type: string + type: object + providers: + items: + description: ProviderInfo represents a single provider from + the providers endpoint. + properties: + api: + type: string + config: + x-kubernetes-preserve-unknown-fields: true + health: + description: ProviderHealthStatus represents the health + status of a provider. + properties: + message: + type: string + status: + type: string + required: + - message + - status + type: object + provider_id: + type: string + provider_type: + type: string + required: + - api + - config + - health + - provider_id + - provider_type + type: object + type: array + type: object + phase: + description: Phase is the current phase of the distribution. + enum: + - Pending + - Initializing + - Ready + - Failed + - Terminating + type: string + resolvedDistribution: + description: ResolvedDistribution tracks the resolved image and config + source. + properties: + configHash: + description: ConfigHash is the SHA-256 hash of the base config + used. + type: string + configSource: + description: 'ConfigSource is the origin of the base config: "embedded" + or "oci-label".' + type: string + image: + description: Image is the resolved container image reference. + type: string + type: object + routeURL: + description: RouteURL is the external URL (when expose is enabled). + type: string + serviceURL: + description: ServiceURL is the internal Kubernetes service URL. + type: string + version: + description: Version contains version information. + properties: + lastUpdated: + description: LastUpdated is when the version information was last + updated. + format: date-time + type: string + llamaStackServerVersion: + description: LlamaStackServerVersion is the version of the LlamaStack + server. + type: string + operatorVersion: + description: OperatorVersion is the version of the operator. + type: string + type: object + type: object + required: + - spec + type: object + served: true storage: true subresources: status: {}