From f10b97912469d376f54a5e5d8415810369622300 Mon Sep 17 00:00:00 2001 From: lanycrost Date: Wed, 15 Oct 2025 15:46:37 +0400 Subject: [PATCH] feat(operator): harden controllers, enable webhooks, enforce timeouts; align CEL and RBAC; CI tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reconcile: add per‑reconcile timeouts; propagate ctx to I/O; unify status patching - Realtime Engram/Impulse/Story/Run: finalize handling, Service/Deployment/StatefulSet reconciliation, SA usage - Webhooks: enable in manager; validate size limits (≤1MiB total), inline caps, schema checks; StepRun/Story/Engram/Impulse validators - Storage: no controller offload; set SDK envs (BUBU_MAX_INLINE_SIZE, BUBU_STORAGE_, BUBU_GRPC) - Indexing: add field indexers for story refs, templates, engram refs - RBAC: least‑privilege; roles and bindings without bind/escalate - CEL: evaluator wiring and helpers improvements - CI/Dev: update workflows, Dockerfile, devcontainer; enable healthz/readyz; metrics security options - CRDs: update Engram/Impulse/StoryRun/StepRun schemas and columns - go.mod/sum: dependency updates --- .devcontainer/devcontainer.json | 2 +- .github/workflows/docker.yml | 6 +- .github/workflows/lint.yml | 7 +- .github/workflows/release-please.yml | 2 +- .github/workflows/test-e2e.yml | 7 +- .github/workflows/test.yml | 7 +- CODE_OF_CONDUCT.md | 9 +- Dockerfile | 2 +- README.md | 55 ++--- SECURITY.md | 10 +- SUPPORT.md | 16 ++ cmd/main.go | 6 +- config/crd/bases/bubustack.io_engrams.yaml | 1 + config/crd/bases/bubustack.io_impulses.yaml | 1 + go.mod | 49 ++--- go.sum | 126 +++++++----- internal/config/operator.go | 107 ++++++---- internal/config/resolver.go | 188 ++++++++++-------- .../catalog/engramtemplate_controller.go | 8 +- .../catalog/impulsetemplate_controller.go | 32 +-- internal/controller/engram_controller.go | 4 +- internal/controller/impulse_controller.go | 51 ++--- .../controller/realtime_engram_controller.go | 111 ++++++----- internal/controller/runs/dag.go | 56 +++--- internal/controller/runs/rbac.go | 10 +- internal/controller/runs/step_executor.go | 52 ++--- .../controller/runs/steprun_controller.go | 121 ++++++----- .../controller/runs/storyrun_controller.go | 58 +++--- internal/controller/story_controller.go | 120 +++++------ internal/webhook/v1alpha1/impulse_webhook.go | 155 +++++++++------ pkg/cel/cache.go | 40 +++- pkg/cel/evaluator.go | 10 +- pkg/cel/expressions.go | 34 ++-- pkg/cel/helpers.go | 42 ++-- pkg/conditions/conditions.go | 40 +++- pkg/enums/enums.go | 4 +- pkg/logging/structured.go | 51 +++-- pkg/metrics/controller_metrics.go | 29 +-- pkg/observability/interfaces.go | 2 +- 39 files changed, 918 insertions(+), 713 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22fcb40..6f70a68 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "docker.io/golang:1.24-bookworm", + "image": "docker.io/golang:1.25-bookworm", "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/devcontainers/features/git:1": {} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8a57aa2..4ee3086 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -63,7 +63,7 @@ jobs: needs: build steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Build image for scanning uses: docker/build-push-action@v6 @@ -81,7 +81,7 @@ jobs: severity: 'CRITICAL,HIGH' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 187b5b9..f5eba4f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,16 +4,19 @@ on: push: pull_request: +permissions: + contents: read + jobs: lint: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c44cf6e..6639a0f 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -34,7 +34,7 @@ jobs: # Build and push Docker images when release is created - name: Checkout code if: ${{ steps.release.outputs.release_created }} - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker Buildx if: ${{ steps.release.outputs.release_created }} diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 68fd1ed..abce605 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -4,16 +4,19 @@ on: push: pull_request: +permissions: + contents: read + jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc2e80d..9dacbcf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,16 +4,19 @@ on: push: pull_request: +permissions: + contents: read + jobs: test: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ddbff29..e35a8fd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -46,15 +46,18 @@ We agree to restrict the following behaviors in our community. Instances, threat Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. -When an incident does occur, it is important to report it promptly. To report a possible violation, **[NOTE: describe your means of reporting here.]** +When an incident does occur, it is important to report it promptly. To report a possible violation, please contact the Community Moderators via one of the following channels: + +- Email: community@bubustack.com +- GitHub Discussions: https://github.com/bubustack/bobrapet/discussions (select the Community Moderation category) + +If you are uncomfortable reporting publicly, email is preferred. We aim to acknowledge reports within 72 hours and will keep reporters updated as appropriate. Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. ## Addressing and Repairing Harm -**[NOTE: The remedies and repairs outlined below are suggestions based on best practices in code of conduct enforcement. If your community has its own established enforcement process, be sure to edit this section to describe your own policies.]** - If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 1) Warning diff --git a/Dockerfile b/Dockerfile index 3845d60..aa16ee7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24-bookworm AS builder +FROM golang:1.25-bookworm AS builder ARG TARGETOS ARG TARGETARCH diff --git a/README.md b/README.md index ee63598..bd07afa 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/bubustack/bobrapet.svg)](https://pkg.go.dev/github.com/bubustack/bobrapet) [![Go Report Card](https://goreportcard.com/badge/github.com/bubustack/bobrapet)](https://goreportcard.com/report/github.com/bubustack/bobrapet) -Bobrapet is a powerful, cloud-native workflow engine for orchestrating complex AI and data processing pipelines on Kubernetes. It leverages the declarative power of Custom Resource Definitions (CRDs) to let you define, manage, and execute multi-step, event-driven workflows with unparalleled flexibility and control. +Bobrapet is a powerful, cloud-native workflow engine for orchestrating complex AI and data processing pipelines on Kubernetes. It leverages the declarative power of Custom Resource Definitions (CRDs) to let you define, manage, and execute multi-step, event-driven workflows with flexibility and control. -For full product docs, visit: https://bubustack.io/docs/ +Quick links: +- Operator docs: https://bubustack.io/docs/bobrapet +- Quickstart: https://bubustack.io/docs/bobrapet/guides/quickstart +- CRD reference: https://bubustack.io/docs/bobrapet/reference/crds ## 🌟 Key Features @@ -19,14 +22,8 @@ For full product docs, visit: https://bubustack.io/docs/ ## 🏗️ Architecture -The `bobrapet` operator is engineered for robustness and maintainability, following best practices for Kubernetes controller design. The core `StoryRun` controller, for example, is built on a modular, sub-reconciler pattern: - -- **Main Controller**: Acts as a lean, high-level orchestrator. -- **RBAC Manager**: Manages all RBAC-related resources (`ServiceAccount`, `Role`, `RoleBinding`). -- **DAG Reconciler**: Contains the entire workflow state machine, handling state synchronization, dependency analysis, and scheduling. -- **Step Executor**: Manages the specific logic for launching different types of steps (`engram`, `executeStory`, etc.). - -This clean separation of concerns makes the operator highly scalable, testable, and easy to extend. +High-level architecture, patterns, and controller internals are documented on the website: +- Overview and architecture: https://bubustack.io/docs/bobrapet/explanations/architecture ## 📚 Core Concepts @@ -39,32 +36,8 @@ This clean separation of concerns makes the operator highly scalable, testable, ## 🧰 Workflow Primitives -Beyond running custom `Engrams`, `Story` resources can use a rich set of built-in primitives for advanced control flow: - -- **`loop`**: Iterate over a list and expand a template step per item. - - `with.items`: CEL‑resolvable data (evaluated with `inputs`, `steps` contexts) - - `with.template`: a single `Step` to instantiate per item - - Limits: max 100 iterations; creates child `StepRun`s and records them under `status.primitiveChildren[step]`; marks the loop step Running ("Loop expanded"). - -- **`parallel`**: Run multiple steps concurrently. - - `with.steps[]`: array of `Step` entries; each branch’s `with` is CEL‑resolved with `inputs` and `steps` - - Creates sibling `StepRun`s; marks the parallel step Running ("Parallel block expanded"). - -- **`stop`**: Terminate the workflow early. - - `with.phase`: one of `Succeeded|Failed|Canceled` (defaults to `Succeeded`) - - `with.message`: optional human message - - Sets `StoryRun.status.phase/message` and returns. - -- **`executeStory`**: Run another `Story` as a sub‑workflow. - - `with.storyRef`: `{ name, namespace? }` - - Current status: placeholder; marks step Succeeded with a message. - -- **`condition`, `switch`, `setData`, `transform`, `filter`, `mergeData`**: - - Batch path: controller marks these primitives Succeeded with outputs available (no pod launch). - - Evidence: batch primitive completion (internal/controller/runs/step_executor.go:49-51) - - Streaming path: `transform` is evaluated in the Hub (CEL over payload/inputs) and forwarded downstream. - -- API declares additional types (`wait`, `throttle`, `batch`, `gate`) for future use. +See the guides for primitives, batch vs. streaming, impulses, and storage configuration: +- Guides: https://bubustack.io/docs/bobrapet/guides ## 🚀 Quick Start @@ -129,13 +102,9 @@ kubectl get stepruns -l bubustack.io/storyrun=summarize-k8s-docs ## Environment variables (operator-injected; consumed by SDK) -- Identity: `BUBU_STORY_NAME`, `BUBU_STORYRUN_ID`, `BUBU_STEP_NAME`, `BUBU_STEPRUN_NAME`, `BUBU_STEPRUN_NAMESPACE`, `BUBU_STARTED_AT` -- Inputs/Config: `BUBU_INPUTS`, `BUBU_CONFIG`, `BUBU_EXECUTION_MODE` -- Storage: `BUBU_MAX_INLINE_SIZE`, `BUBU_STORAGE_PROVIDER`, `BUBU_STORAGE_TIMEOUT`, `BUBU_STORAGE_S3_BUCKET`, `BUBU_STORAGE_S3_REGION`, `BUBU_STORAGE_S3_ENDPOINT` -- gRPC (server/client): `BUBU_GRPC_PORT`, `BUBU_GRPC_MAX_RECV_BYTES`, `BUBU_GRPC_MAX_SEND_BYTES`, `BUBU_GRPC_CLIENT_MAX_RECV_BYTES`, `BUBU_GRPC_CLIENT_MAX_SEND_BYTES`, `BUBU_GRPC_MESSAGE_TIMEOUT`, `BUBU_GRPC_CHANNEL_SEND_TIMEOUT`, `BUBU_GRPC_RECONNECT_BASE_BACKOFF`, `BUBU_GRPC_RECONNECT_MAX_BACKOFF`, `BUBU_GRPC_RECONNECT_MAX_RETRIES` -- TLS (optional): `BUBU_GRPC_TLS_CERT_FILE`, `BUBU_GRPC_TLS_KEY_FILE`, `BUBU_GRPC_CA_FILE`, `BUBU_GRPC_CLIENT_TLS`, `BUBU_GRPC_CLIENT_CERT_FILE`, `BUBU_GRPC_CLIENT_KEY_FILE`, `BUBU_GRPC_REQUIRE_TLS` - -See detailed tables in `bubustack.io/docs/reference`. +For complete environment variable listings and defaults, see the operator configuration and transport reference: +- Operator config: https://bubustack.io/docs/bobrapet/reference/config +- gRPC transport: https://bubustack.io/docs/bobrapet/reference/grpc ## 🛠️ Local Development diff --git a/SECURITY.md b/SECURITY.md index ed7c10a..f76c7bc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,9 @@ ## Supported versions -We provide security updates for the latest released version of the operator. Please ensure you are using a supported version to receive security patches. +We provide security updates for the latest released minor of the operator. Please ensure you are using a supported version to receive security patches. We generally support the latest minor and the immediately previous minor. + +Supported Kubernetes versions: we aim to support N-2 of upstream stable releases. For example, when Kubernetes 1.31 is current, we target 1.31, 1.30, 1.29. See `config/crd/kustomization.yaml` and CI matrices for exact compatibility. ## Reporting a vulnerability @@ -18,7 +20,7 @@ When reporting a vulnerability, please provide the following information: - **A clear description** of the vulnerability and its potential impact. - **Steps to reproduce** the vulnerability, including any example code, scripts, or configurations. -- **The version(s) of the SDK** affected. +- **The version(s) of the operator** affected. - **Your contact information** for us to follow up with you. ## Disclosure process @@ -27,7 +29,9 @@ When reporting a vulnerability, please provide the following information: 2. **Confirmation**: We will acknowledge your report within 48 hours. 3. **Investigation**: We will investigate the vulnerability and determine its scope and impact. We may contact you for additional information during this phase. 4. **Fix**: We will develop a patch for the vulnerability. -5. **Disclosure**: We will create a security advisory, issue a CVE, and release a new version with the patch. We will credit you for your discovery unless you prefer to remain anonymous. +5. **Disclosure**: We will create a security advisory, issue a CVE (if applicable), and release a new version with the patch. We will credit you for your discovery unless you prefer to remain anonymous. + +We aim to resolve high severity vulnerabilities within 30 days, medium within 60 days, and low within 90 days, subject to complexity and scope. We'll keep you informed of progress. We aim to resolve all vulnerabilities as quickly as possible. The timeline for a fix and disclosure will vary depending on the complexity and severity of the vulnerability. We will keep you informed of our progress throughout the process. diff --git a/SUPPORT.md b/SUPPORT.md index ab01ea2..75b34c7 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -15,6 +15,17 @@ For questions, discussions, and community support, please use the following chan - **GitHub Issues**: For bug reports and feature requests, please open an issue: https://github.com/bubustack/bobrapet/issues - **GitHub Discussions**: For general questions and sharing your projects, please use Discussions: https://github.com/bubustack/bobrapet/discussions +### Triage and response SLAs (best effort) + +- We triage new GitHub issues Mon–Fri during business hours. +- Initial response target: within 2 business days. +- Security reports follow the timelines in SECURITY.md. + +### Supported versions + +- We generally support the latest minor release and the previous minor release of the operator. +- Kubernetes compatibility target: N-2 upstream stable releases. + ## Commercial support For commercial support, including enterprise features, dedicated support channels, and SLAs, contact [sales@bubustack.com](mailto:sales@bubustack.com). @@ -22,3 +33,8 @@ For commercial support, including enterprise features, dedicated support channel ## Reporting security vulnerabilities To report a security vulnerability, please follow the instructions in our [Security Policy](./SECURITY.md). + +### Related documentation + +- Troubleshooting: https://bubustack.io/docs/bobrapet/troubleshooting +- Known issues: https://github.com/bubustack/bobrapet/issues?q=is%3Aissue+is%3Aopen+label%3Abug diff --git a/cmd/main.go b/cmd/main.go index cb5a463..91a372b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -194,7 +194,11 @@ func main() { managerCtx := ctrl.SetupSignalHandler() setup.SetupIndexers(managerCtx, mgr) - operatorConfigManager := config.NewOperatorConfigManager(mgr.GetClient(), "bobrapet-system", "bobrapet-operator-config") + operatorConfigManager := config.NewOperatorConfigManager( + mgr.GetClient(), + "bobrapet-system", + "bobrapet-operator-config", + ) setupLog.Info("Operator configuration manager initialized") if err := mgr.Add(operatorConfigManager); err != nil { setupLog.Error(err, "unable to add operator config manager to manager") diff --git a/config/crd/bases/bubustack.io_engrams.yaml b/config/crd/bases/bubustack.io_engrams.yaml index ac572fb..dcfac1a 100644 --- a/config/crd/bases/bubustack.io_engrams.yaml +++ b/config/crd/bases/bubustack.io_engrams.yaml @@ -358,6 +358,7 @@ spec: Pending → Running → {Succeeded|Failed|Canceled|Compensated|Paused|Blocked|Scheduling|Timeout|Aborted} Some resources may also support Paused for manual intervention scenarios. + nolint:lll enum: - Pending - Running diff --git a/config/crd/bases/bubustack.io_impulses.yaml b/config/crd/bases/bubustack.io_impulses.yaml index 719c9f1..fdbc102 100644 --- a/config/crd/bases/bubustack.io_impulses.yaml +++ b/config/crd/bases/bubustack.io_impulses.yaml @@ -464,6 +464,7 @@ spec: Pending → Running → {Succeeded|Failed|Canceled|Compensated|Paused|Blocked|Scheduling|Timeout|Aborted} Some resources may also support Paused for manual intervention scenarios. + nolint:lll enum: - Pending - Running diff --git a/go.mod b/go.mod index aeb6ae1..33b309a 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,22 @@ module github.com/bubustack/bobrapet go 1.24.5 require ( - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/google/cel-go v0.26.1 - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.36.1 - github.com/prometheus/client_golang v1.22.0 + github.com/onsi/ginkgo/v2 v2.26.0 + github.com/onsi/gomega v1.38.2 + github.com/prometheus/client_golang v1.23.2 github.com/xeipuuv/gojsonschema v1.2.0 - google.golang.org/protobuf v1.36.5 - k8s.io/api v0.34.0 - k8s.io/apimachinery v0.34.0 - k8s.io/client-go v0.34.0 - sigs.k8s.io/controller-runtime v0.22.1 + google.golang.org/protobuf v1.36.10 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.3 ) require ( cel.dev/expr v0.24.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -39,7 +40,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -51,9 +52,9 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect @@ -69,19 +70,21 @@ require ( go.opentelemetry.io/otel/sdk v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/tools v0.36.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect @@ -89,9 +92,9 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.34.0 // indirect - k8s.io/apiserver v0.34.0 // indirect - k8s.io/component-base v0.34.0 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apiserver v0.34.1 // indirect + k8s.io/component-base v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/go.sum b/go.sum index f1ed758..67a5a25 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -27,9 +29,15 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo= +github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -44,6 +52,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -59,8 +69,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= @@ -69,6 +79,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -86,6 +98,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -94,22 +110,24 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= +github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -128,8 +146,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -160,6 +186,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -177,38 +205,40 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -221,8 +251,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -233,18 +263,18 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= @@ -253,8 +283,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/internal/config/operator.go b/internal/config/operator.go index b14c5f2..800ea32 100644 --- a/internal/config/operator.go +++ b/internal/config/operator.go @@ -30,6 +30,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +const strTrue = "true" + // OperatorConfig holds the configuration for the operator type OperatorConfig struct { Controller ControllerConfig `json:"controller,omitempty"` @@ -78,9 +80,9 @@ type OperatorConfigManager struct { } // NewOperatorConfigManager creates a new configuration manager -func NewOperatorConfigManager(client client.Client, namespace, configMapName string) *OperatorConfigManager { +func NewOperatorConfigManager(k8sClient client.Client, namespace, configMapName string) *OperatorConfigManager { manager := &OperatorConfigManager{ - client: client, + client: k8sClient, namespace: namespace, configMapName: configMapName, defaultConfig: DefaultOperatorConfig(), @@ -114,9 +116,7 @@ func (m *OperatorConfigManager) loadAndParseConfigMap(ctx context.Context) (*Ope } config := &OperatorConfig{} - if err := m.parseConfigMap(configMap, config); err != nil { - return nil, fmt.Errorf("failed to unmarshal operator config: %w", err) - } + m.parseConfigMap(configMap, config) return config, nil } @@ -124,12 +124,12 @@ func (m *OperatorConfigManager) loadAndParseConfigMap(ctx context.Context) (*Ope // Start runs the configuration synchronization loop. // It implements the controller-runtime manager.Runnable interface. func (m *OperatorConfigManager) Start(ctx context.Context) error { - log := log.FromContext(ctx).WithName("config-manager") - log.Info("Starting operator config synchronizer") + logger := log.FromContext(ctx).WithName("config-manager") + logger.Info("Starting operator config synchronizer") // Immediately try to sync on startup if err := m.sync(ctx); err != nil { - log.Error(err, "Initial configuration sync failed") + logger.Error(err, "Initial configuration sync failed") // Depending on strictness, we might want to return the error // and prevent the manager from starting if the config is critical. } @@ -140,11 +140,11 @@ func (m *OperatorConfigManager) Start(ctx context.Context) error { for { select { case <-ctx.Done(): - log.Info("Stopping operator config synchronizer") + logger.Info("Stopping operator config synchronizer") return nil case <-ticker.C: if err := m.sync(ctx); err != nil { - log.Error(err, "Failed to sync operator configuration") + logger.Error(err, "Failed to sync operator configuration") } } } @@ -152,7 +152,7 @@ func (m *OperatorConfigManager) Start(ctx context.Context) error { // sync performs a single configuration synchronization. func (m *OperatorConfigManager) sync(ctx context.Context) error { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) newConfig, err := m.loadAndParseConfigMap(ctx) if err != nil { return fmt.Errorf("failed to load operator configuration: %w", err) @@ -162,13 +162,29 @@ func (m *OperatorConfigManager) sync(ctx context.Context) error { defer m.mu.Unlock() m.currentConfig = newConfig m.lastSyncTime = time.Now() - log.Info("Successfully synced operator configuration") + logger.Info("Successfully synced operator configuration") return nil } // parseConfigMap parses a ConfigMap into OperatorConfig -func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *OperatorConfig) error { - // Controller Configuration +func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *OperatorConfig) { + parseControllerTimings(cm, config) + parseImageConfig(cm, config) + parseResourceLimits(cm, config) + parseRetryAndTimeouts(cm, config) + parseLoopConfig(cm, config) + parseSecurityConfig(cm, config) + parseJobConfig(cm, config) + parseCELConfig(cm, config) + parseTelemetryConfig(cm, config) + parseDebugConfig(cm, config) + parseEngramDefaults(cm, config) + parseStoryRunConfig(cm, config) +} + +// --- helpers below keep parseConfigMap readable and reduce cyclomatic complexity --- + +func parseControllerTimings(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["controller.max-concurrent-reconciles"]; exists { if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 { config.Controller.MaxConcurrentReconciles = parsed @@ -204,8 +220,9 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O config.Controller.DefaultEngramGRPCPort = parsed } } +} - // Image Configuration +func parseImageConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["images.default-engram"]; exists { config.Controller.DefaultEngramImage = val } @@ -214,16 +231,17 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O } if val, exists := cm.Data["images.pull-policy"]; exists { switch val { - case "Always": + case string(corev1.PullAlways): config.Controller.ImagePullPolicy = corev1.PullAlways - case "Never": + case string(corev1.PullNever): config.Controller.ImagePullPolicy = corev1.PullNever - case "IfNotPresent": + case string(corev1.PullIfNotPresent): config.Controller.ImagePullPolicy = corev1.PullIfNotPresent } } +} - // Resource Limits +func parseResourceLimits(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["resources.engram.cpu-request"]; exists { config.Controller.EngramCPURequest = val } @@ -236,8 +254,9 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O if val, exists := cm.Data["resources.engram.memory-limit"]; exists { config.Controller.EngramMemoryLimit = val } +} - // Retry and Timeout Configuration +func parseRetryAndTimeouts(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["retry.max-retries"]; exists { if parsed, err := strconv.Atoi(val); err == nil && parsed >= 0 { config.Controller.MaxRetries = parsed @@ -273,8 +292,9 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O config.Controller.ConditionalTimeout = parsed } } +} - // Loop Processing Configuration +func parseLoopConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["loop.max-iterations"]; exists { if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 { config.Controller.MaxLoopIterations = parsed @@ -300,28 +320,30 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O config.Controller.MaxConcurrencyLimit = parsed } } +} + +func parseSecurityConfig(cm *corev1.ConfigMap, config *OperatorConfig) { - // Security Configuration if val, exists := cm.Data["security.run-as-non-root"]; exists { - config.Controller.RunAsNonRoot = val == "true" + config.Controller.RunAsNonRoot = val == strTrue } if val, exists := cm.Data["security.read-only-root-filesystem"]; exists { - config.Controller.ReadOnlyRootFilesystem = val == "true" + config.Controller.ReadOnlyRootFilesystem = val == strTrue } if val, exists := cm.Data["security.allow-privilege-escalation"]; exists { - config.Controller.AllowPrivilegeEscalation = val == "true" + config.Controller.AllowPrivilegeEscalation = val == strTrue } if val, exists := cm.Data["security.run-as-user"]; exists { if parsed, err := strconv.ParseInt(val, 10, 64); err == nil { config.Controller.RunAsUser = parsed } } - if val, exists := cm.Data["security.automount-service-account-token"]; exists { - config.Controller.AutomountServiceAccountToken = val == "true" + config.Controller.AutomountServiceAccountToken = val == strTrue } +} - // Job Configuration +func parseJobConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["job.backoff-limit"]; exists { if parsed, err := strconv.ParseInt(val, 10, 32); err == nil { config.Controller.JobBackoffLimit = int32(parsed) @@ -332,8 +354,9 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O config.Controller.TTLSecondsAfterFinished = int32(parsed) } } +} - // CEL Configuration +func parseCELConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["cel.evaluation-timeout"]; exists { if parsed, err := time.ParseDuration(val); err == nil { config.Controller.CELEvaluationTimeout = parsed @@ -345,41 +368,43 @@ func (ocm *OperatorConfigManager) parseConfigMap(cm *corev1.ConfigMap, config *O } } if val, exists := cm.Data["cel.enable-macros"]; exists { - config.Controller.CELEnableMacros = val == "true" + config.Controller.CELEnableMacros = val == strTrue } +} - // Telemetry Configuration +func parseTelemetryConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["telemetry.enabled"]; exists { - config.Controller.TelemetryEnabled = val == "true" + config.Controller.TelemetryEnabled = val == strTrue } if val, exists := cm.Data["telemetry.trace-propagation"]; exists { - config.Controller.TracePropagationEnabled = val == "true" + config.Controller.TracePropagationEnabled = val == strTrue } +} - // Development/Debug Configuration +func parseDebugConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["debug.enable-verbose-logging"]; exists { - config.Controller.EnableVerboseLogging = val == "true" + config.Controller.EnableVerboseLogging = val == strTrue } if val, exists := cm.Data["debug.enable-step-output-logging"]; exists { - config.Controller.EnableStepOutputLogging = val == "true" + config.Controller.EnableStepOutputLogging = val == strTrue } if val, exists := cm.Data["debug.enable-metrics"]; exists { - config.Controller.EnableMetrics = val == "true" + config.Controller.EnableMetrics = val == strTrue } +} - // Engram default inline size (bytes) +func parseEngramDefaults(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["engram.default-max-inline-size"]; exists { if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 { config.Controller.Engram.EngramControllerConfig.DefaultMaxInlineSize = parsed } } +} - // StoryRun Configuration +func parseStoryRunConfig(cm *corev1.ConfigMap, config *OperatorConfig) { if val, exists := cm.Data["storyrun.max-inline-inputs-size"]; exists { if parsed, err := strconv.Atoi(val); err == nil && parsed > 0 { config.Controller.StoryRun.MaxInlineInputsSize = parsed } } - - return nil } diff --git a/internal/config/resolver.go b/internal/config/resolver.go index 31b3327..470c1e7 100644 --- a/internal/config/resolver.go +++ b/internal/config/resolver.go @@ -34,14 +34,14 @@ import ( // Resolver resolves configuration with hierarchical priority // Priority (highest to lowest): StepRun > Story.Policy > Namespace > Controller Config type Resolver struct { - client client.Client + k8sClient client.Client configManager *OperatorConfigManager } // NewResolver creates a new configuration resolver -func NewResolver(client client.Client, configManager *OperatorConfigManager) *Resolver { +func NewResolver(k8sClient client.Client, configManager *OperatorConfigManager) *Resolver { return &Resolver{ - client: client, + k8sClient: k8sClient, configManager: configManager, } } @@ -124,7 +124,7 @@ func (cr *Resolver) ResolveExecutionConfig(ctx context.Context, step *runsv1alph // getOperatorDefaults initializes the configuration with values from the operator's config map. func (cr *Resolver) getOperatorDefaults(config *OperatorConfig) *ResolvedExecutionConfig { return &ResolvedExecutionConfig{ - ImagePullPolicy: corev1.PullPolicy(config.Controller.ImagePullPolicy), + ImagePullPolicy: config.Controller.ImagePullPolicy, Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse(config.Controller.DefaultCPURequest), @@ -142,7 +142,7 @@ func (cr *Resolver) getOperatorDefaults(config *OperatorConfig) *ResolvedExecuti RunAsUser: config.Controller.RunAsUser, BackoffLimit: config.Controller.JobBackoffLimit, TTLSecondsAfterFinished: config.Controller.TTLSecondsAfterFinished, - RestartPolicy: corev1.RestartPolicy(config.Controller.JobRestartPolicy), + RestartPolicy: config.Controller.JobRestartPolicy, ServiceAccountName: config.Controller.ServiceAccountName, DefaultStepTimeout: config.Controller.DefaultStepTimeout, MaxRetries: config.Controller.MaxRetries, @@ -156,80 +156,96 @@ func (cr *Resolver) applyEngramTemplateConfig(template *catalogv1alpha1.EngramTe if template == nil { return } + cr.applyTemplateImage(template, config) + exec := template.Spec.Execution + if exec == nil { + return + } + cr.applyTemplateImagePullPolicy(exec, config) + cr.applyTemplateResources(exec, config) + cr.applyTemplateSecurity(exec, config) + cr.applyTemplateJob(exec, config) + cr.applyTemplateTimeoutRetry(exec, config) + cr.applyTemplateProbes(exec, config) + cr.applyTemplateService(exec, config) +} - // Image from template spec directly +func (cr *Resolver) applyTemplateImage(template *catalogv1alpha1.EngramTemplate, config *ResolvedExecutionConfig) { if template.Spec.Image != "" { config.Image = template.Spec.Image } +} - if template.Spec.Execution == nil { - return - } - exec := template.Spec.Execution - - // Image +func (cr *Resolver) applyTemplateImagePullPolicy(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { if exec.Images != nil && exec.Images.PullPolicy != nil { switch *exec.Images.PullPolicy { - case "Always": + case string(corev1.PullAlways): config.ImagePullPolicy = corev1.PullAlways - case "Never": + case string(corev1.PullNever): config.ImagePullPolicy = corev1.PullNever - case "IfNotPresent": + case string(corev1.PullIfNotPresent): config.ImagePullPolicy = corev1.PullIfNotPresent } } +} - // Resources - if exec.Resources != nil { - if exec.Resources.RecommendedCPURequest != nil { - config.Resources.Requests[corev1.ResourceCPU] = resource.MustParse(*exec.Resources.RecommendedCPURequest) - } - if exec.Resources.RecommendedCPULimit != nil { - config.Resources.Limits[corev1.ResourceCPU] = resource.MustParse(*exec.Resources.RecommendedCPULimit) - } - if exec.Resources.RecommendedMemoryRequest != nil { - config.Resources.Requests[corev1.ResourceMemory] = resource.MustParse(*exec.Resources.RecommendedMemoryRequest) - } - if exec.Resources.RecommendedMemoryLimit != nil { - config.Resources.Limits[corev1.ResourceMemory] = resource.MustParse(*exec.Resources.RecommendedMemoryLimit) - } +func (cr *Resolver) applyTemplateResources(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { + if exec.Resources == nil { + return + } + if exec.Resources.RecommendedCPURequest != nil { + config.Resources.Requests[corev1.ResourceCPU] = resource.MustParse(*exec.Resources.RecommendedCPURequest) + } + if exec.Resources.RecommendedCPULimit != nil { + config.Resources.Limits[corev1.ResourceCPU] = resource.MustParse(*exec.Resources.RecommendedCPULimit) } + if exec.Resources.RecommendedMemoryRequest != nil { + config.Resources.Requests[corev1.ResourceMemory] = resource.MustParse(*exec.Resources.RecommendedMemoryRequest) + } + if exec.Resources.RecommendedMemoryLimit != nil { + config.Resources.Limits[corev1.ResourceMemory] = resource.MustParse(*exec.Resources.RecommendedMemoryLimit) + } +} - // Security - if exec.Security != nil { - if exec.Security.RequiresNonRoot != nil { - config.RunAsNonRoot = *exec.Security.RequiresNonRoot - } - if exec.Security.RequiresReadOnlyRoot != nil { - config.ReadOnlyRootFilesystem = *exec.Security.RequiresReadOnlyRoot - } - if exec.Security.RequiresNoPrivilegeEscalation != nil { - config.AllowPrivilegeEscalation = !*exec.Security.RequiresNoPrivilegeEscalation - } - if exec.Security.RecommendedRunAsUser != nil { - config.RunAsUser = *exec.Security.RecommendedRunAsUser - } +func (cr *Resolver) applyTemplateSecurity(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { + if exec.Security == nil { + return + } + if exec.Security.RequiresNonRoot != nil { + config.RunAsNonRoot = *exec.Security.RequiresNonRoot } + if exec.Security.RequiresReadOnlyRoot != nil { + config.ReadOnlyRootFilesystem = *exec.Security.RequiresReadOnlyRoot + } + if exec.Security.RequiresNoPrivilegeEscalation != nil { + config.AllowPrivilegeEscalation = !*exec.Security.RequiresNoPrivilegeEscalation + } + if exec.Security.RecommendedRunAsUser != nil { + config.RunAsUser = *exec.Security.RecommendedRunAsUser + } +} - // Job - if exec.Job != nil { - if exec.Job.RecommendedBackoffLimit != nil { - config.BackoffLimit = *exec.Job.RecommendedBackoffLimit - } - if exec.Job.RecommendedTTLSecondsAfterFinished != nil { - config.TTLSecondsAfterFinished = *exec.Job.RecommendedTTLSecondsAfterFinished - } - if exec.Job.RecommendedRestartPolicy != nil { - switch *exec.Job.RecommendedRestartPolicy { - case "Never": - config.RestartPolicy = corev1.RestartPolicyNever - case "OnFailure": - config.RestartPolicy = corev1.RestartPolicyOnFailure - } +func (cr *Resolver) applyTemplateJob(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { + if exec.Job == nil { + return + } + if exec.Job.RecommendedBackoffLimit != nil { + config.BackoffLimit = *exec.Job.RecommendedBackoffLimit + } + if exec.Job.RecommendedTTLSecondsAfterFinished != nil { + config.TTLSecondsAfterFinished = *exec.Job.RecommendedTTLSecondsAfterFinished + } + if exec.Job.RecommendedRestartPolicy != nil { + switch *exec.Job.RecommendedRestartPolicy { + case "Never": + config.RestartPolicy = corev1.RestartPolicyNever + case "OnFailure": + config.RestartPolicy = corev1.RestartPolicyOnFailure } } +} - // Timeout & Retry +func (cr *Resolver) applyTemplateTimeoutRetry(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { if exec.Timeout != nil { if d, err := time.ParseDuration(*exec.Timeout); err == nil { config.DefaultStepTimeout = d @@ -238,31 +254,35 @@ func (cr *Resolver) applyEngramTemplateConfig(template *catalogv1alpha1.EngramTe if exec.Retry != nil && exec.Retry.RecommendedMaxRetries != nil { config.MaxRetries = *exec.Retry.RecommendedMaxRetries } +} - // Health Check Probes - if exec.Probes != nil { - if exec.Probes.Liveness != nil { - config.LivenessProbe = exec.Probes.Liveness.DeepCopy() - } - if exec.Probes.Readiness != nil { - config.ReadinessProbe = exec.Probes.Readiness.DeepCopy() - } - if exec.Probes.Startup != nil { - config.StartupProbe = exec.Probes.Startup.DeepCopy() - } +func (cr *Resolver) applyTemplateProbes(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { + if exec.Probes == nil { + return + } + if exec.Probes.Liveness != nil { + config.LivenessProbe = exec.Probes.Liveness.DeepCopy() + } + if exec.Probes.Readiness != nil { + config.ReadinessProbe = exec.Probes.Readiness.DeepCopy() } + if exec.Probes.Startup != nil { + config.StartupProbe = exec.Probes.Startup.DeepCopy() + } +} - // Service Configuration - if exec.Service != nil && len(exec.Service.Ports) > 0 { - config.ServicePorts = make([]corev1.ServicePort, 0, len(exec.Service.Ports)) - for _, p := range exec.Service.Ports { - config.ServicePorts = append(config.ServicePorts, corev1.ServicePort{ - Name: p.Name, - Protocol: corev1.Protocol(p.Protocol), - Port: p.Port, - TargetPort: intstr.FromInt(int(p.TargetPort)), - }) - } +func (cr *Resolver) applyTemplateService(exec *catalogv1alpha1.TemplateExecutionPolicy, config *ResolvedExecutionConfig) { + if exec.Service == nil || len(exec.Service.Ports) == 0 { + return + } + config.ServicePorts = make([]corev1.ServicePort, 0, len(exec.Service.Ports)) + for _, p := range exec.Service.Ports { + config.ServicePorts = append(config.ServicePorts, corev1.ServicePort{ + Name: p.Name, + Protocol: corev1.Protocol(p.Protocol), + Port: p.Port, + TargetPort: intstr.FromInt(int(p.TargetPort)), + }) } } @@ -337,11 +357,11 @@ func (cr *Resolver) ApplyExecutionOverrides(overrides *v1alpha1.ExecutionOverrid } if overrides.ImagePullPolicy != nil { switch *overrides.ImagePullPolicy { - case "Always": + case string(corev1.PullAlways): config.ImagePullPolicy = corev1.PullAlways - case "Never": + case string(corev1.PullNever): config.ImagePullPolicy = corev1.PullNever - case "IfNotPresent": + case string(corev1.PullIfNotPresent): config.ImagePullPolicy = corev1.PullIfNotPresent } } diff --git a/internal/controller/catalog/engramtemplate_controller.go b/internal/controller/catalog/engramtemplate_controller.go index 34aac99..2ac18c4 100644 --- a/internal/controller/catalog/engramtemplate_controller.go +++ b/internal/controller/catalog/engramtemplate_controller.go @@ -42,9 +42,9 @@ type EngramTemplateReconciler struct { config.ControllerDependencies } -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates/finalizers,verbs=update +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=engramtemplates/finalizers,verbs=update // Reconcile validates and manages EngramTemplate lifecycle func (r *EngramTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -157,7 +157,7 @@ func (r *EngramTemplateReconciler) validateJSONSchema(schemaBytes []byte) error } // Basic JSON validation - var schema interface{} + var schema any if err := json.Unmarshal(schemaBytes, &schema); err != nil { return fmt.Errorf("invalid JSON: %w", err) } diff --git a/internal/controller/catalog/impulsetemplate_controller.go b/internal/controller/catalog/impulsetemplate_controller.go index 9f41e9a..b5ef9a1 100644 --- a/internal/controller/catalog/impulsetemplate_controller.go +++ b/internal/controller/catalog/impulsetemplate_controller.go @@ -48,9 +48,9 @@ type ImpulseTemplateReconciler struct { config.ControllerDependencies } -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates/finalizers,verbs=update +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=catalog.bubustack.io,resources=impulsetemplates/finalizers,verbs=update // Reconcile validates and manages ImpulseTemplate lifecycle func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -64,7 +64,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ }() var template catalogv1alpha1.ImpulseTemplate - if err := r.ControllerDependencies.Client.Get(ctx, req.NamespacedName, &template); err != nil { + if err := r.Get(ctx, req.NamespacedName, &template); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -76,7 +76,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if template.Spec.Image == "" { r.updateErrorStatus(&template, "image is required") rl.ReconcileError(fmt.Errorf("image missing"), "Image is required for ImpulseTemplate") - if err := r.updateStatusWithRetry(ctx, &template, 3); err != nil { + if err := r.updateStatusWithRetry(ctx, &template); err != nil { return ctrl.Result{RequeueAfter: 5 * time.Second}, err } return ctrl.Result{}, nil @@ -85,7 +85,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if template.Spec.Version == "" { r.updateErrorStatus(&template, "version is required") rl.ReconcileError(fmt.Errorf("version missing"), "Version is required for ImpulseTemplate") - if err := r.updateStatusWithRetry(ctx, &template, 3); err != nil { + if err := r.updateStatusWithRetry(ctx, &template); err != nil { return ctrl.Result{RequeueAfter: 5 * time.Second}, err } return ctrl.Result{}, nil @@ -99,7 +99,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if !slices.Contains(validImpulseModes, mode) { r.updateErrorStatus(&template, fmt.Sprintf("invalid supported mode '%s' for impulse template (must be deployment or statefulset)", mode)) rl.ReconcileError(fmt.Errorf("invalid supported mode: %s", mode), "Invalid supported mode for ImpulseTemplate") - if err := r.updateStatusWithRetry(ctx, &template, 3); err != nil { + if err := r.updateStatusWithRetry(ctx, &template); err != nil { return ctrl.Result{RequeueAfter: 5 * time.Second}, err } return ctrl.Result{}, nil @@ -112,7 +112,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err := r.validateJSONSchema(template.Spec.ContextSchema.Raw); err != nil { r.updateErrorStatus(&template, fmt.Sprintf("invalid context schema: %v", err)) rl.ReconcileError(err, "Invalid context schema") - if updateErr := r.updateStatusWithRetry(ctx, &template, 3); updateErr != nil { + if updateErr := r.updateStatusWithRetry(ctx, &template); updateErr != nil { templateLogger.Error(updateErr, "failed to update status after schema validation error") } return ctrl.Result{RequeueAfter: 10 * time.Second}, nil @@ -124,7 +124,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err := r.validateJSONSchema(template.Spec.ConfigSchema.Raw); err != nil { r.updateErrorStatus(&template, fmt.Sprintf("invalid config schema: %v", err)) rl.ReconcileError(err, "Invalid config schema") - if updateErr := r.updateStatusWithRetry(ctx, &template, 3); updateErr != nil { + if updateErr := r.updateStatusWithRetry(ctx, &template); updateErr != nil { templateLogger.Error(updateErr, "failed to update status after schema validation error") } return ctrl.Result{RequeueAfter: 10 * time.Second}, nil @@ -144,7 +144,7 @@ func (r *ImpulseTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ template.Status.UsageCount = int32(len(impulses.Items)) } - if err := r.updateStatusWithRetry(ctx, &template, 3); err != nil { + if err := r.updateStatusWithRetry(ctx, &template); err != nil { rl.ReconcileError(err, "Failed to update ImpulseTemplate status") return ctrl.Result{RequeueAfter: 5 * time.Second}, err } @@ -165,7 +165,7 @@ func (r *ImpulseTemplateReconciler) validateJSONSchema(schemaBytes []byte) error } // Basic JSON validation - var schema interface{} + var schema any if err := json.Unmarshal(schemaBytes, &schema); err != nil { return fmt.Errorf("invalid JSON: %w", err) } @@ -191,12 +191,12 @@ func (r *ImpulseTemplateReconciler) updateReadyStatus(template *catalogv1alpha1. // remove custom setCondition helper; ConditionManager is used for consistency // updateStatusWithRetry performs status update with retry logic -func (r *ImpulseTemplateReconciler) updateStatusWithRetry(ctx context.Context, template *catalogv1alpha1.ImpulseTemplate, maxRetries int) error { +func (r *ImpulseTemplateReconciler) updateStatusWithRetry(ctx context.Context, template *catalogv1alpha1.ImpulseTemplate) error { var lastErr error - for i := 0; i < maxRetries; i++ { - if err := r.ControllerDependencies.Client.Status().Update(ctx, template); err != nil { + for i := 0; i < 3; i++ { + if err := r.Status().Update(ctx, template); err != nil { lastErr = err - if i < maxRetries-1 { + if i < 2 { // Use a timer that respects context cancellation sleepDuration := time.Duration(100*(1< 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -127,7 +127,7 @@ func (r *EngramReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Op mapTemplateToEngrams := func(ctx context.Context, obj client.Object) []reconcile.Request { var engrams bubushv1alpha1.EngramList // List all engrams in all namespaces that might reference this cluster-scoped template. - if err := r.Client.List(ctx, &engrams, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { + if err := r.List(ctx, &engrams, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { // In case of error, log it but don't panic the controller. // An empty list will be returned, and reconciliation will depend on engram updates. log := logging.NewControllerLogger(ctx, "engram-mapper") diff --git a/internal/controller/impulse_controller.go b/internal/controller/impulse_controller.go index ac24da0..b15706f 100644 --- a/internal/controller/impulse_controller.go +++ b/internal/controller/impulse_controller.go @@ -64,7 +64,7 @@ type ImpulseReconciler struct { func (r *ImpulseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logging.NewReconcileLogger(ctx, "impulse").WithValues("impulse", req.NamespacedName) - timeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout + timeout := r.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -72,11 +72,11 @@ func (r *ImpulseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } var impulse v1alpha1.Impulse - if err := r.ControllerDependencies.Client.Get(ctx, req.NamespacedName, &impulse); err != nil { + if err := r.Get(ctx, req.NamespacedName, &impulse); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - if !impulse.ObjectMeta.DeletionTimestamp.IsZero() { + if !impulse.DeletionTimestamp.IsZero() { // Handle deletion if finalizers are added. return ctrl.Result{}, nil } @@ -87,7 +87,7 @@ func (r *ImpulseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // Fetch the template var template catalogv1alpha1.ImpulseTemplate - if err := r.ControllerDependencies.Client.Get(ctx, types.NamespacedName{Name: impulse.Spec.TemplateRef.Name}, &template); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: impulse.Spec.TemplateRef.Name}, &template); err != nil { if errors.IsNotFound(err) { log.Error(err, "ImpulseTemplate not found") // Emit event for user visibility (guard recorder for tests) @@ -115,7 +115,7 @@ func (r *ImpulseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct }, }, } - resolvedConfig, err := r.ControllerDependencies.ConfigResolver.ResolveExecutionConfig(ctx, nil, nil, nil, engramTemplate) + resolvedConfig, err := r.ConfigResolver.ResolveExecutionConfig(ctx, nil, nil, nil, engramTemplate) if err != nil { err := r.setImpulsePhase(ctx, &impulse, enums.PhaseFailed, fmt.Sprintf("Failed to resolve configuration: %s", err)) if err != nil { @@ -132,24 +132,27 @@ func (r *ImpulseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // Update status based on the Deployment's state - return r.updateImpulseStatus(ctx, &impulse, deployment) + if err := r.updateImpulseStatus(ctx, &impulse, deployment); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } -func (r *ImpulseReconciler) reconcileDeployment(ctx context.Context, impulse *v1alpha1.Impulse, template *catalogv1alpha1.ImpulseTemplate, config *config.ResolvedExecutionConfig) (*appsv1.Deployment, error) { +func (r *ImpulseReconciler) reconcileDeployment(ctx context.Context, impulse *v1alpha1.Impulse, template *catalogv1alpha1.ImpulseTemplate, execCfg *config.ResolvedExecutionConfig) (*appsv1.Deployment, error) { log := logging.NewReconcileLogger(ctx, "impulse-deployment").WithValues("impulse", impulse.Name) deployment := &appsv1.Deployment{} deploymentName := fmt.Sprintf("%s-%s-impulse", impulse.Name, impulse.Spec.TemplateRef.Name) deploymentKey := types.NamespacedName{Name: deploymentName, Namespace: impulse.Namespace} - err := r.ControllerDependencies.Client.Get(ctx, deploymentKey, deployment) + err := r.Get(ctx, deploymentKey, deployment) if err != nil { if errors.IsNotFound(err) { // Create it - newDeployment := r.buildDeploymentForImpulse(impulse, template, config) - if err := controllerutil.SetControllerReference(impulse, newDeployment, r.ControllerDependencies.Scheme); err != nil { + newDeployment := r.buildDeploymentForImpulse(impulse, template, execCfg) + if err := controllerutil.SetControllerReference(impulse, newDeployment, r.Scheme); err != nil { return nil, fmt.Errorf("failed to set owner reference on Deployment: %w", err) } - if err := r.ControllerDependencies.Client.Create(ctx, newDeployment); err != nil { + if err := r.Create(ctx, newDeployment); err != nil { return nil, fmt.Errorf("failed to create Deployment: %w", err) } log.Info("Created new Deployment for Impulse") @@ -159,14 +162,14 @@ func (r *ImpulseReconciler) reconcileDeployment(ctx context.Context, impulse *v1 } // It exists, so check for updates. - desiredDeployment := r.buildDeploymentForImpulse(impulse, template, config) + desiredDeployment := r.buildDeploymentForImpulse(impulse, template, execCfg) // A simple way to check for differences is to compare the pod templates. // For more complex scenarios, a 3-way merge or more specific field comparisons might be needed. if !reflect.DeepEqual(deployment.Spec.Template, desiredDeployment.Spec.Template) { log.Info("Deployment spec has changed, updating.") original := deployment.DeepCopy() deployment.Spec.Template = desiredDeployment.Spec.Template - if err := r.ControllerDependencies.Client.Patch(ctx, deployment, client.MergeFrom(original)); err != nil { + if err := r.Patch(ctx, deployment, client.MergeFrom(original)); err != nil { return nil, fmt.Errorf("failed to update Deployment: %w", err) } } @@ -174,7 +177,7 @@ func (r *ImpulseReconciler) reconcileDeployment(ctx context.Context, impulse *v1 return deployment, nil } -func (r *ImpulseReconciler) updateImpulseStatus(ctx context.Context, impulse *v1alpha1.Impulse, deployment *appsv1.Deployment) (ctrl.Result, error) { +func (r *ImpulseReconciler) updateImpulseStatus(ctx context.Context, impulse *v1alpha1.Impulse, deployment *appsv1.Deployment) error { var newPhase enums.Phase var message string @@ -191,7 +194,7 @@ func (r *ImpulseReconciler) updateImpulseStatus(ctx context.Context, impulse *v1 } if impulse.Status.Phase != newPhase || impulse.Status.ReadyReplicas != deployment.Status.ReadyReplicas { - err := patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, impulse, func(obj client.Object) { + err := patch.RetryableStatusPatch(ctx, r.Client, impulse, func(obj client.Object) { i := obj.(*v1alpha1.Impulse) i.Status.Phase = newPhase i.Status.ObservedGeneration = i.Generation @@ -206,15 +209,15 @@ func (r *ImpulseReconciler) updateImpulseStatus(ctx context.Context, impulse *v1 } }) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to update impulse status: %w", err) + return fmt.Errorf("failed to update impulse status: %w", err) } } - return ctrl.Result{}, nil + return nil } func (r *ImpulseReconciler) setImpulsePhase(ctx context.Context, impulse *v1alpha1.Impulse, phase enums.Phase, message string) error { - return patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, impulse, func(obj client.Object) { + return patch.RetryableStatusPatch(ctx, r.Client, impulse, func(obj client.Object) { i := obj.(*v1alpha1.Impulse) i.Status.Phase = phase i.Status.ObservedGeneration = i.Generation @@ -223,8 +226,8 @@ func (r *ImpulseReconciler) setImpulsePhase(ctx context.Context, impulse *v1alph // Use standard reasons from pkg/conditions to avoid hard-coded literals var ( - status metav1.ConditionStatus = metav1.ConditionFalse - reason string = conditions.ReasonReconciling + status = metav1.ConditionFalse + reason string ) switch phase { case enums.PhaseRunning: @@ -246,7 +249,7 @@ func (r *ImpulseReconciler) setImpulsePhase(ctx context.Context, impulse *v1alph } // buildDeploymentForImpulse creates a new Deployment object for an Impulse -func (r *ImpulseReconciler) buildDeploymentForImpulse(impulse *v1alpha1.Impulse, template *catalogv1alpha1.ImpulseTemplate, config *config.ResolvedExecutionConfig) *appsv1.Deployment { +func (r *ImpulseReconciler) buildDeploymentForImpulse(impulse *v1alpha1.Impulse, _ *catalogv1alpha1.ImpulseTemplate, execCfg *config.ResolvedExecutionConfig) *appsv1.Deployment { deploymentName := fmt.Sprintf("%s-%s-impulse", impulse.Name, impulse.Spec.TemplateRef.Name) labels := map[string]string{ "app.kubernetes.io/name": "bobrapet-impulse", @@ -285,10 +288,10 @@ func (r *ImpulseReconciler) buildDeploymentForImpulse(impulse *v1alpha1.Impulse, Labels: labels, }, Spec: corev1.PodSpec{ - ServiceAccountName: config.ServiceAccountName, + ServiceAccountName: execCfg.ServiceAccountName, Containers: []corev1.Container{{ Name: "impulse", - Image: config.Image, + Image: execCfg.Image, Env: envVars, }}, }, @@ -316,7 +319,7 @@ func (r *ImpulseReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.O func (r *ImpulseReconciler) mapImpulseTemplateToImpulses(ctx context.Context, obj client.Object) []reconcile.Request { var impulses v1alpha1.ImpulseList - if err := r.ControllerDependencies.Client.List(ctx, &impulses, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { + if err := r.List(ctx, &impulses, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { // Handle error return nil } diff --git a/internal/controller/realtime_engram_controller.go b/internal/controller/realtime_engram_controller.go index 302ec10..3351444 100644 --- a/internal/controller/realtime_engram_controller.go +++ b/internal/controller/realtime_engram_controller.go @@ -34,14 +34,14 @@ import ( // maybeConfigureTLSEnvAndMounts attempts to mount a TLS secret and set SDK TLS env vars. // If the named secret exists, it mounts it to /var/run/tls and sets server/client envs. // containerIndex selects which container to mutate (0 = engram container). -func (r *RealtimeEngramReconciler) maybeConfigureTLSEnvAndMounts(ctx context.Context, namespace, secretName string, podSpec *corev1.PodTemplateSpec, containerIndex int) bool { +func (r *RealtimeEngramReconciler) maybeConfigureTLSEnvAndMounts(ctx context.Context, namespace, secretName string, podSpec *corev1.PodTemplateSpec, containerIndex int) error { if podSpec == nil || len(podSpec.Spec.Containers) <= containerIndex { - return false + return fmt.Errorf("podSpec is nil or container index is out of range") } // Probe secret existence var sec corev1.Secret if err := r.Get(ctx, types.NamespacedName{Namespace: namespace, Name: secretName}, &sec); err != nil { - return false + return err } // Add volume if not present volumeName := "engram-tls" @@ -86,7 +86,8 @@ func (r *RealtimeEngramReconciler) maybeConfigureTLSEnvAndMounts(ctx context.Con corev1.EnvVar{Name: "BUBU_GRPC_CLIENT_KEY_FILE", Value: mountPath + "/tls.key"}, corev1.EnvVar{Name: "BUBU_GRPC_REQUIRE_TLS", Value: "true"}, ) - return true + + return nil } // getTLSSecretName reads TLS secret name from annotations in priority order: @@ -116,12 +117,12 @@ type RealtimeEngramReconciler struct { config.ControllerDependencies } -//+kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=bubustack.io,resources=engrams/finalizers,verbs=update -//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=bubustack.io,resources=engrams/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete func (r *RealtimeEngramReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { log := logging.NewReconcileLogger(ctx, "realtime-engram").WithValues("engram", req.NamespacedName) @@ -131,7 +132,7 @@ func (r *RealtimeEngramReconciler) Reconcile(ctx context.Context, req ctrl.Reque }() // Bound reconcile duration - timeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout + timeout := r.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -158,7 +159,10 @@ func (r *RealtimeEngramReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Handle deletion if !engram.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, &engram) + if err := r.reconcileDelete(ctx, &engram); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } // Add finalizer if it doesn't exist @@ -273,7 +277,7 @@ func (r *RealtimeEngramReconciler) getEngramTemplate(ctx context.Context, engram return template, nil } -func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram *v1alpha1.Engram) (ctrl.Result, error) { +func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram *v1alpha1.Engram) error { log := logging.NewReconcileLogger(ctx, "realtime-engram").WithValues("engram", engram.Name) log.Info("Reconciling deletion for realtime Engram") @@ -283,12 +287,12 @@ func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram * if err == nil { if err := r.Delete(ctx, deployment); err != nil && !errors.IsNotFound(err) { log.Error(err, "Failed to delete owned Deployment") - return ctrl.Result{}, err + return err } log.Info("Deleted owned Deployment") } else if !errors.IsNotFound(err) { log.Error(err, "Failed to get owned Deployment for deletion") - return ctrl.Result{}, err + return err } // Delete owned StatefulSet @@ -297,12 +301,12 @@ func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram * if err == nil { if err := r.Delete(ctx, sts); err != nil && !errors.IsNotFound(err) { log.Error(err, "Failed to delete owned StatefulSet") - return ctrl.Result{}, err + return err } log.Info("Deleted owned StatefulSet") } else if !errors.IsNotFound(err) { log.Error(err, "Failed to get owned StatefulSet for deletion") - return ctrl.Result{}, err + return err } // Delete owned Service @@ -311,12 +315,12 @@ func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram * if err == nil { if err := r.Delete(ctx, service); err != nil && !errors.IsNotFound(err) { log.Error(err, "Failed to delete owned Service") - return ctrl.Result{}, err + return err } log.Info("Deleted owned Service") } else if !errors.IsNotFound(err) { log.Error(err, "Failed to get owned Service for deletion") - return ctrl.Result{}, err + return err } // All owned resources are deleted, remove the finalizer @@ -324,21 +328,21 @@ func (r *RealtimeEngramReconciler) reconcileDelete(ctx context.Context, engram * controllerutil.RemoveFinalizer(engram, RealtimeEngramFinalizer) if err := r.Update(ctx, engram); err != nil { log.Error(err, "Failed to remove finalizer") - return ctrl.Result{}, err + return err } log.Info("Removed finalizer") } - return ctrl.Result{}, nil + return nil } -func (r *RealtimeEngramReconciler) reconcileDeployment(ctx context.Context, engram *v1alpha1.Engram, execConfig *config.ResolvedExecutionConfig) error { +func (r *RealtimeEngramReconciler) reconcileDeployment(ctx context.Context, engram *v1alpha1.Engram, execCfg *config.ResolvedExecutionConfig) error { log := logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name) deployment := &appsv1.Deployment{} err := r.Get(ctx, types.NamespacedName{Name: engram.Name, Namespace: engram.Namespace}, deployment) if err != nil && errors.IsNotFound(err) { log.Info("Creating a new Deployment", "Deployment.Namespace", engram.Namespace, "Deployment.Name", engram.Name) - newDep := r.deploymentForEngram(ctx, engram, execConfig) + newDep := r.deploymentForEngram(ctx, engram, execCfg) if err := r.Create(ctx, newDep); err != nil { log.Error(err, "Failed to create new Deployment") return err @@ -349,17 +353,17 @@ func (r *RealtimeEngramReconciler) reconcileDeployment(ctx context.Context, engr } // Update logic - desiredDep := r.deploymentForEngram(ctx, engram, execConfig) + desiredDep := r.deploymentForEngram(ctx, engram, execCfg) if !reflect.DeepEqual(deployment.Spec.Template, desiredDep.Spec.Template) || *deployment.Spec.Replicas != *desiredDep.Spec.Replicas { log.Info("Deployment spec differs, updating deployment") // Retry on conflict using a fresh GET + merge to avoid clobbering cluster-managed fields key := types.NamespacedName{Name: engram.Name, Namespace: engram.Namespace} - if err := r.Client.Get(ctx, key, deployment); err != nil { + if err := r.Get(ctx, key, deployment); err != nil { return err } original := deployment.DeepCopy() deployment.Spec = desiredDep.Spec - if err := r.Client.Patch(ctx, deployment, client.MergeFrom(original)); err != nil { + if err := r.Patch(ctx, deployment, client.MergeFrom(original)); err != nil { log.Error(err, "Failed to patch Deployment") return err } @@ -390,12 +394,12 @@ func (r *RealtimeEngramReconciler) reconcileStatefulSet(ctx context.Context, eng if !reflect.DeepEqual(sts.Spec.Template, desired.Spec.Template) || sts.Spec.ServiceName != desired.Spec.ServiceName { log.Info("StatefulSet spec differs, updating statefulset") key := types.NamespacedName{Name: engram.Name, Namespace: engram.Namespace} - if err := r.Client.Get(ctx, key, sts); err != nil { + if err := r.Get(ctx, key, sts); err != nil { return err } original := sts.DeepCopy() sts.Spec = desired.Spec - if err := r.Client.Patch(ctx, sts, client.MergeFrom(original)); err != nil { + if err := r.Patch(ctx, sts, client.MergeFrom(original)); err != nil { log.Error(err, "Failed to patch StatefulSet") return err } @@ -486,7 +490,10 @@ func (r *RealtimeEngramReconciler) statefulSetForEngram(ctx context.Context, eng // Configure TLS via user-provided secret if annotated if sec := getTLSSecretName(&engram.ObjectMeta); sec != "" { - r.maybeConfigureTLSEnvAndMounts(ctx, engram.Namespace, sec, &podSpec, 0) + if err := r.maybeConfigureTLSEnvAndMounts(ctx, engram.Namespace, sec, &podSpec, 0); err != nil { + // Log and continue without TLS rather than returning a nil StatefulSet + logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Error(err, "Failed to configure TLS mounts/envs for StatefulSet") + } } sts := &appsv1.StatefulSet{ @@ -502,7 +509,9 @@ func (r *RealtimeEngramReconciler) statefulSetForEngram(ctx context.Context, eng Template: podSpec, }, } - ctrl.SetControllerReference(engram, sts, r.Scheme) + if err := ctrl.SetControllerReference(engram, sts, r.Scheme); err != nil { + logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Error(err, "failed to set controller reference on StatefulSet") + } return sts } @@ -528,7 +537,7 @@ func (r *RealtimeEngramReconciler) reconcileService(ctx context.Context, engram err := r.Get(ctx, types.NamespacedName{Name: engram.Name, Namespace: engram.Namespace}, service) if err != nil && errors.IsNotFound(err) { log.Info("Creating a new Service", "Service.Namespace", engram.Namespace, "Service.Name", engram.Name) - newSvc := r.serviceForEngram(engram, execConfig) + newSvc := r.serviceForEngram(ctx, engram, execConfig) if err := r.Create(ctx, newSvc); err != nil { log.Error(err, "Failed to create new Service") return err @@ -539,7 +548,7 @@ func (r *RealtimeEngramReconciler) reconcileService(ctx context.Context, engram } // Update logic - desiredSvc := r.serviceForEngram(engram, execConfig) + desiredSvc := r.serviceForEngram(ctx, engram, execConfig) // Preserve the ClusterIP desiredSvc.Spec.ClusterIP = service.Spec.ClusterIP // Preserve the ResourceVersion to avoid update conflicts @@ -548,12 +557,12 @@ func (r *RealtimeEngramReconciler) reconcileService(ctx context.Context, engram if !reflect.DeepEqual(service.Spec, desiredSvc.Spec) { log.Info("Service spec differs, updating service") key := types.NamespacedName{Name: engram.Name, Namespace: engram.Namespace} - if err := r.Client.Get(ctx, key, service); err != nil { + if err := r.Get(ctx, key, service); err != nil { return err } original := service.DeepCopy() service.Spec = desiredSvc.Spec - if err := r.Client.Patch(ctx, service, client.MergeFrom(original)); err != nil { + if err := r.Patch(ctx, service, client.MergeFrom(original)); err != nil { log.Error(err, "Failed to patch Service") return err } @@ -562,7 +571,7 @@ func (r *RealtimeEngramReconciler) reconcileService(ctx context.Context, engram return nil } -func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engram *v1alpha1.Engram, config *config.ResolvedExecutionConfig) *appsv1.Deployment { +func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engram *v1alpha1.Engram, execCfg *config.ResolvedExecutionConfig) *appsv1.Deployment { labels := map[string]string{"app": engram.Name, "bubustack.io/engram": engram.Name} replicas := int32(1) @@ -584,14 +593,14 @@ func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engr Labels: labels, }, Spec: corev1.PodSpec{ - ServiceAccountName: config.ServiceAccountName, + ServiceAccountName: execCfg.ServiceAccountName, TerminationGracePeriodSeconds: &terminationGracePeriod, - SecurityContext: config.ToPodSecurityContext(), + SecurityContext: execCfg.ToPodSecurityContext(), Containers: []corev1.Container{{ - Image: config.Image, + Image: execCfg.Image, Name: "engram", - ImagePullPolicy: config.ImagePullPolicy, - SecurityContext: config.ToContainerSecurityContext(), + ImagePullPolicy: execCfg.ImagePullPolicy, + SecurityContext: execCfg.ToContainerSecurityContext(), Ports: []corev1.ContainerPort{{ ContainerPort: int32(r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort), Name: "grpc", @@ -624,15 +633,15 @@ func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engr {Name: "BUBU_GRPC_MESSAGE_TIMEOUT", Value: fmt.Sprintf("%ds", engramConfig.DefaultMessageTimeoutSeconds)}, {Name: "BUBU_GRPC_CHANNEL_SEND_TIMEOUT", Value: fmt.Sprintf("%ds", engramConfig.DefaultMessageTimeoutSeconds)}, // Re-use message timeout }, configEnvVars...), - LivenessProbe: config.LivenessProbe, - ReadinessProbe: config.ReadinessProbe, - StartupProbe: config.StartupProbe, + LivenessProbe: execCfg.LivenessProbe, + ReadinessProbe: execCfg.ReadinessProbe, + StartupProbe: execCfg.StartupProbe, }}, }, } // Handle object storage configuration - if storagePolicy := config.Storage; storagePolicy != nil && storagePolicy.S3 != nil { + if storagePolicy := execCfg.Storage; storagePolicy != nil && storagePolicy.S3 != nil { s3Config := storagePolicy.S3 // log with reconcile context logging.NewControllerLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Info("Configuring pod for S3 object storage access", "bucket", s3Config.Bucket) @@ -663,7 +672,9 @@ func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engr // Configure TLS via user-provided secret if annotated if sec := getTLSSecretName(&engram.ObjectMeta); sec != "" { - r.maybeConfigureTLSEnvAndMounts(ctx, engram.Namespace, sec, &podSpec, 0) + if err := r.maybeConfigureTLSEnvAndMounts(ctx, engram.Namespace, sec, &podSpec, 0); err != nil { + logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Error(err, "Failed to configure TLS mounts/envs for Deployment") + } } dep := &appsv1.Deployment{ @@ -679,11 +690,13 @@ func (r *RealtimeEngramReconciler) deploymentForEngram(ctx context.Context, engr Template: podSpec, }, } - ctrl.SetControllerReference(engram, dep, r.Scheme) + if err := ctrl.SetControllerReference(engram, dep, r.Scheme); err != nil { + logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Error(err, "failed to set controller reference on Deployment") + } return dep } -func (r *RealtimeEngramReconciler) serviceForEngram(engram *v1alpha1.Engram, config *config.ResolvedExecutionConfig) *corev1.Service { +func (r *RealtimeEngramReconciler) serviceForEngram(ctx context.Context, engram *v1alpha1.Engram, _ *config.ResolvedExecutionConfig) *corev1.Service { labels := map[string]string{"app": engram.Name, "bubustack.io/engram": engram.Name} svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -706,7 +719,9 @@ func (r *RealtimeEngramReconciler) serviceForEngram(engram *v1alpha1.Engram, con // Optional: make endpoints available before readiness for DNS svc.Spec.PublishNotReadyAddresses = true } - ctrl.SetControllerReference(engram, svc, r.Scheme) + if err := ctrl.SetControllerReference(engram, svc, r.Scheme); err != nil { + logging.NewReconcileLogger(ctx, "realtime_engram").WithValues("engram", engram.Name).Error(err, "failed to set controller reference on Service") + } return svc } diff --git a/internal/controller/runs/dag.go b/internal/controller/runs/dag.go index 4a9ac44..9a97621 100644 --- a/internal/controller/runs/dag.go +++ b/internal/controller/runs/dag.go @@ -30,10 +30,10 @@ type DAGReconciler struct { } // NewDAGReconciler creates a new DAGReconciler. -func NewDAGReconciler(client client.Client, cel *cel.Evaluator, stepExecutor *StepExecutor, configResolver *config.Resolver) *DAGReconciler { +func NewDAGReconciler(k8sClient client.Client, celEval *cel.Evaluator, stepExecutor *StepExecutor, configResolver *config.Resolver) *DAGReconciler { return &DAGReconciler{ - Client: client, - CEL: cel, + Client: k8sClient, + CEL: celEval, StepExecutor: stepExecutor, ConfigResolver: configResolver, } @@ -63,9 +63,7 @@ func (r *DAGReconciler) Reconcile(ctx context.Context, srun *runsv1alpha1.StoryR } for i := 0; i < len(story.Spec.Steps)+1; i++ { // +1 to allow one final check after all steps are processed // Sync state from synchronous sub-stories - if updated, err := r.checkSyncSubStories(ctx, srun, story); err != nil { - return ctrl.Result{}, err - } else if updated { + if updated := r.checkSyncSubStories(ctx, srun, story); updated { // If a sub-story status was synced, we must re-fetch the outputs // Inefficient to re-list all step runs, but acceptable for now as sub-stories are less common. // A future improvement would be to only fetch the single sub-story's output and merge it in. @@ -171,7 +169,7 @@ func (r *DAGReconciler) syncStateFromStepRuns(ctx context.Context, srun *runsv1a return &stepRunList, nil } -func (r *DAGReconciler) checkSyncSubStories(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story) (bool, error) { +func (r *DAGReconciler) checkSyncSubStories(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story) bool { log := logging.NewReconcileLogger(ctx, "storyrun-dag-substory") var statusUpdated bool for i := range story.Spec.Steps { @@ -216,31 +214,25 @@ func (r *DAGReconciler) checkSyncSubStories(ctx context.Context, srun *runsv1alp // Manually copy sub-story output into parent's step state. // This is still risky if the sub-story output is large, but acceptable for now // as it's less common than step outputs. A future enhancement should offload this. - if subRun.Status.Phase == enums.PhaseSucceeded && subRun.Status.Output != nil { - // NOTE: This does NOT write to the deprecated StepOutputs field. - // This output is available to subsequent steps via the on-demand resolver. - } + // Note: sub-story outputs are not propagated to StoryRun to avoid large status payloads. + // They are resolved on-demand by downstream evaluators using the sub-StoryRun object. } } } - return statusUpdated, nil + return statusUpdated } -func (r *DAGReconciler) findAndLaunchReadySteps(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, completedSteps, runningSteps map[string]bool, priorStepOutputs map[string]interface{}) ([]*bubuv1alpha1.Step, []*bubuv1alpha1.Step, error) { +func (r *DAGReconciler) findAndLaunchReadySteps(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, completedSteps, runningSteps map[string]bool, priorStepOutputs map[string]any) ([]*bubuv1alpha1.Step, []*bubuv1alpha1.Step, error) { log := logging.NewReconcileLogger(ctx, "storyrun-dag-launcher") dependencies, _ := buildDependencyGraphs(story.Spec.Steps) storyRunInputs, _ := getStoryRunInputs(srun) - vars := map[string]interface{}{ + vars := map[string]any{ "inputs": storyRunInputs, "steps": priorStepOutputs, } - readySteps, skippedSteps, err := r.findReadySteps(ctx, story.Spec.Steps, completedSteps, runningSteps, dependencies, vars) - if err != nil { - log.Error(err, "Failed to find ready steps") - return nil, nil, err - } + readySteps, skippedSteps := r.findReadySteps(ctx, story.Spec.Steps, completedSteps, runningSteps, dependencies, vars) // Handle skipped steps by updating their status. for _, step := range skippedSteps { @@ -265,9 +257,9 @@ func (r *DAGReconciler) findAndLaunchReadySteps(ctx context.Context, srun *runsv // getPriorStepOutputs fetches the outputs of all previously completed steps in the same StoryRun. // It serves as the single source of truth for step outputs within the DAG reconciler. -func getPriorStepOutputs(ctx context.Context, c client.Client, srun *runsv1alpha1.StoryRun, stepRunList *runsv1alpha1.StepRunList) (map[string]interface{}, error) { +func getPriorStepOutputs(ctx context.Context, c client.Client, srun *runsv1alpha1.StoryRun, stepRunList *runsv1alpha1.StepRunList) (map[string]any, error) { log := logging.NewReconcileLogger(ctx, "dag-output-resolver") - outputs := make(map[string]interface{}) + outputs := make(map[string]any) // The StoryRun's status.stepOutputs field is deprecated and no longer used. // Instead, we always resolve outputs by listing the child StepRun objects, @@ -289,12 +281,12 @@ func getPriorStepOutputs(ctx context.Context, c client.Client, srun *runsv1alpha } if sr.Status.Phase == enums.PhaseSucceeded && sr.Status.Output != nil { - var outputData map[string]interface{} + var outputData map[string]any if err := json.Unmarshal(sr.Status.Output.Raw, &outputData); err != nil { log.Error(err, "Failed to unmarshal output from prior StepRun during fallback", "step", sr.Spec.StepID) continue } - outputs[sr.Spec.StepID] = map[string]interface{}{"outputs": outputData} + outputs[sr.Spec.StepID] = map[string]any{"outputs": outputData} } } @@ -313,12 +305,12 @@ func getPriorStepOutputs(ctx context.Context, c client.Client, srun *runsv1alpha } if subRun.Status.Output != nil { - var outputData map[string]interface{} + var outputData map[string]any if err := json.Unmarshal(subRun.Status.Output.Raw, &outputData); err != nil { log.Error(err, "Failed to unmarshal output from sub-StoryRun", "step", stepID) continue } - outputs[stepID] = map[string]interface{}{"outputs": outputData} + outputs[stepID] = map[string]any{"outputs": outputData} } } } @@ -334,7 +326,7 @@ func getPriorStepOutputs(ctx context.Context, c client.Client, srun *runsv1alpha return outputs, nil } -func (r *DAGReconciler) findReadySteps(ctx context.Context, steps []bubuv1alpha1.Step, completed, running map[string]bool, dependencies map[string]map[string]bool, vars map[string]interface{}) ([]*bubuv1alpha1.Step, []*bubuv1alpha1.Step, error) { +func (r *DAGReconciler) findReadySteps(ctx context.Context, steps []bubuv1alpha1.Step, completed, running map[string]bool, dependencies map[string]map[string]bool, vars map[string]any) ([]*bubuv1alpha1.Step, []*bubuv1alpha1.Step) { var ready, skipped []*bubuv1alpha1.Step for i := range steps { @@ -370,7 +362,7 @@ func (r *DAGReconciler) findReadySteps(ctx context.Context, steps []bubuv1alpha1 ready = append(ready, step) } } - return ready, skipped, nil + return ready, skipped } // finalizeSuccessfulRun evaluates the Story's output template and sets the final status. @@ -393,12 +385,12 @@ func (r *DAGReconciler) finalizeSuccessfulRun(ctx context.Context, srun *runsv1a return fmt.Errorf("failed to get prior step outputs for output evaluation: %w", err) } - vars := map[string]interface{}{ + vars := map[string]any{ "inputs": storyRunInputs, "steps": priorStepOutputs, } - var outputTemplate map[string]interface{} + var outputTemplate map[string]any if err := json.Unmarshal(story.Spec.Output.Raw, &outputTemplate); err != nil { return fmt.Errorf("failed to unmarshal story output template: %w", err) } @@ -531,11 +523,11 @@ func buildStateMaps(states map[string]runsv1alpha1.StepState) (completed, runnin return } -func getStoryRunInputs(srun *runsv1alpha1.StoryRun) (map[string]interface{}, error) { +func getStoryRunInputs(srun *runsv1alpha1.StoryRun) (map[string]any, error) { if srun.Spec.Inputs == nil { - return make(map[string]interface{}), nil + return make(map[string]any), nil } - var inputs map[string]interface{} + var inputs map[string]any if err := json.Unmarshal(srun.Spec.Inputs.Raw, &inputs); err != nil { return nil, fmt.Errorf("failed to unmarshal storyrun inputs: %w", err) } diff --git a/internal/controller/runs/rbac.go b/internal/controller/runs/rbac.go index 34b3407..8ac9cdd 100644 --- a/internal/controller/runs/rbac.go +++ b/internal/controller/runs/rbac.go @@ -24,8 +24,8 @@ type RBACManager struct { } // NewRBACManager creates a new RBACManager. -func NewRBACManager(client client.Client, scheme *runtime.Scheme) *RBACManager { - return &RBACManager{Client: client, Scheme: scheme} +func NewRBACManager(k8sClient client.Client, scheme *runtime.Scheme) *RBACManager { + return &RBACManager{Client: k8sClient, Scheme: scheme} } // Reconcile ensures the necessary ServiceAccount, Role, and RoleBinding exist for the StoryRun. @@ -88,7 +88,7 @@ func (r *RBACManager) Reconcile(ctx context.Context, storyRun *runsv1alpha1.Stor if err := controllerutil.SetOwnerReference(storyRun, role, r.Scheme); err != nil { return fmt.Errorf("failed to set owner reference on Role: %w", err) } - if err := r.Client.Create(ctx, role); err != nil && !errors.IsAlreadyExists(err) { + if err := r.Create(ctx, role); err != nil && !errors.IsAlreadyExists(err) { return fmt.Errorf("failed to create Role: %w", err) } else if err == nil { log.Info("Created Role for Engram runner", "role", role.Name) @@ -115,7 +115,7 @@ func (r *RBACManager) Reconcile(ctx context.Context, storyRun *runsv1alpha1.Stor if err := controllerutil.SetOwnerReference(storyRun, rb, r.Scheme); err != nil { return fmt.Errorf("failed to set owner reference on RoleBinding: %w", err) } - if err := r.Client.Create(ctx, rb); err != nil && !errors.IsAlreadyExists(err) { + if err := r.Create(ctx, rb); err != nil && !errors.IsAlreadyExists(err) { return fmt.Errorf("failed to create RoleBinding: %w", err) } else if err == nil { log.Info("Created RoleBinding for Engram runner", "roleBinding", rb.Name) @@ -132,7 +132,7 @@ func (r *RBACManager) getStoryForRun(ctx context.Context, srun *runsv1alpha1.Sto Name: srun.Spec.StoryRef.Name, Namespace: srun.Spec.StoryRef.ToNamespacedName(srun).Namespace, } - if err := r.Client.Get(ctx, key, &story); err != nil { + if err := r.Get(ctx, key, &story); err != nil { return nil, err } return &story, nil diff --git a/internal/controller/runs/step_executor.go b/internal/controller/runs/step_executor.go index 0c7e146..2af26ce 100644 --- a/internal/controller/runs/step_executor.go +++ b/internal/controller/runs/step_executor.go @@ -27,12 +27,12 @@ type StepExecutor struct { } // NewStepExecutor creates a new StepExecutor. -func NewStepExecutor(client client.Client, scheme *runtime.Scheme, cel *cel.Evaluator) *StepExecutor { - return &StepExecutor{Client: client, Scheme: scheme, CEL: cel} +func NewStepExecutor(k8sClient client.Client, scheme *runtime.Scheme, celEval *cel.Evaluator) *StepExecutor { + return &StepExecutor{Client: k8sClient, Scheme: scheme, CEL: celEval} } // Execute determines the step type and calls the appropriate execution method. -func (e *StepExecutor) Execute(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]interface{}) error { +func (e *StepExecutor) Execute(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]any) error { // An Engram step is defined by the presence of the 'ref' field. if step.Ref != nil { return e.executeEngramStep(ctx, srun, story, step) @@ -62,7 +62,7 @@ func (e *StepExecutor) Execute(ctx context.Context, srun *runsv1alpha1.StoryRun, func (e *StepExecutor) executeEngramStep(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, step *bubuv1alpha1.Step) error { stepName := fmt.Sprintf("%s-%s", srun.Name, step.Name) var stepRun runsv1alpha1.StepRun - err := e.Client.Get(ctx, types.NamespacedName{Name: stepName, Namespace: srun.Namespace}, &stepRun) + err := e.Get(ctx, types.NamespacedName{Name: stepName, Namespace: srun.Namespace}, &stepRun) if err != nil && errors.IsNotFound(err) { if step.Ref == nil { @@ -71,7 +71,7 @@ func (e *StepExecutor) executeEngramStep(ctx context.Context, srun *runsv1alpha1 // Get the engram to merge the 'with' blocks var engram bubuv1alpha1.Engram - if err := e.Client.Get(ctx, step.Ref.ToNamespacedName(srun), &engram); err != nil { + if err := e.Get(ctx, step.Ref.ToNamespacedName(srun), &engram); err != nil { return fmt.Errorf("failed to get engram '%s' for step '%s': %w", step.Ref.Name, step.Name, err) } @@ -101,12 +101,12 @@ func (e *StepExecutor) executeEngramStep(ctx context.Context, srun *runsv1alpha1 if err := controllerutil.SetControllerReference(srun, &stepRun, e.Scheme); err != nil { return err } - return e.Client.Create(ctx, &stepRun) + return e.Create(ctx, &stepRun) } return err // Return other errors or nil if found } -func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]interface{}) error { +func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.StoryRun, _ *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]any) error { type loopWith struct { Items json.RawMessage `json:"items"` Template bubuv1alpha1.Step `json:"template"` @@ -116,17 +116,17 @@ func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.S return fmt.Errorf("failed to parse 'with' for loop step '%s': %w", step.Name, err) } - var itemsMap interface{} + var itemsMap any if err := json.Unmarshal(config.Items, &itemsMap); err != nil { // fallback to string var str string if err := json.Unmarshal(config.Items, &str); err != nil { return fmt.Errorf("failed to parse 'items' for loop step '%s': %w", step.Name, err) } - itemsMap = map[string]interface{}{"value": str} + itemsMap = map[string]any{"value": str} } - resolvedItemsRaw, err := e.CEL.ResolveWithInputs(ctx, itemsMap.(map[string]interface{}), vars) + resolvedItemsRaw, err := e.CEL.ResolveWithInputs(ctx, itemsMap.(map[string]any), vars) if err != nil { return fmt.Errorf("failed to resolve 'items' for loop step '%s': %w", step.Name, err) } @@ -141,26 +141,26 @@ func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.S return fmt.Errorf("loop step '%s' exceeds maximum of %d iterations", step.Name, maxIterations) } - var childStepRunNames []string + childStepRunNames := make([]string, 0, len(resolvedItems)) for i, item := range resolvedItems { childStepName := fmt.Sprintf("%s-%s-%d", srun.Name, step.Name, i) childStepRunNames = append(childStepRunNames, childStepName) - loopVars := map[string]interface{}{ + loopVars := map[string]any{ "inputs": vars["inputs"], "steps": vars["steps"], "item": item, "index": i, } - var withMap interface{} + var withMap any if config.Template.With != nil { if err := json.Unmarshal(config.Template.With.Raw, &withMap); err != nil { return fmt.Errorf("failed to parse 'with' for loop template in step '%s': %w", step.Name, err) } } - resolvedWith, err := e.CEL.ResolveWithInputs(ctx, withMap.(map[string]interface{}), loopVars) + resolvedWith, err := e.CEL.ResolveWithInputs(ctx, withMap.(map[string]any), loopVars) if err != nil { return fmt.Errorf("failed to resolve 'with' for loop iteration %d in step '%s': %w", i, step.Name, err) } @@ -182,7 +182,7 @@ func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.S if err := controllerutil.SetControllerReference(srun, stepRun, e.Scheme); err != nil { return err } - if err := e.Client.Create(ctx, stepRun); err != nil && !errors.IsAlreadyExists(err) { + if err := e.Create(ctx, stepRun); err != nil && !errors.IsAlreadyExists(err) { return err } } @@ -195,7 +195,7 @@ func (e *StepExecutor) executeLoopStep(ctx context.Context, srun *runsv1alpha1.S return nil } -func (e *StepExecutor) executeParallelStep(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]interface{}) error { +func (e *StepExecutor) executeParallelStep(ctx context.Context, srun *runsv1alpha1.StoryRun, _ *bubuv1alpha1.Story, step *bubuv1alpha1.Step, vars map[string]any) error { type parallelWith struct { Steps []bubuv1alpha1.Step `json:"steps"` } @@ -204,19 +204,19 @@ func (e *StepExecutor) executeParallelStep(ctx context.Context, srun *runsv1alph return fmt.Errorf("failed to parse 'with' for parallel step '%s': %w", step.Name, err) } - var childStepRunNames []string + childStepRunNames := make([]string, 0, len(config.Steps)) for _, childStep := range config.Steps { childStepName := fmt.Sprintf("%s-%s-%s", srun.Name, step.Name, childStep.Name) childStepRunNames = append(childStepRunNames, childStepName) - var withMap interface{} + var withMap any if childStep.With != nil { if err := json.Unmarshal(childStep.With.Raw, &withMap); err != nil { return fmt.Errorf("failed to parse 'with' for parallel branch '%s' in step '%s': %w", childStep.Name, step.Name, err) } } - resolvedWith, err := e.CEL.ResolveWithInputs(ctx, withMap.(map[string]interface{}), vars) + resolvedWith, err := e.CEL.ResolveWithInputs(ctx, withMap.(map[string]any), vars) if err != nil { return fmt.Errorf("failed to resolve 'with' for parallel branch '%s' in step '%s': %w", childStep.Name, step.Name, err) } @@ -238,7 +238,7 @@ func (e *StepExecutor) executeParallelStep(ctx context.Context, srun *runsv1alph if err := controllerutil.SetControllerReference(srun, stepRun, e.Scheme); err != nil { return err } - if err := e.Client.Create(ctx, stepRun); err != nil && !errors.IsAlreadyExists(err) { + if err := e.Create(ctx, stepRun); err != nil && !errors.IsAlreadyExists(err) { return err } } @@ -298,7 +298,7 @@ func (e *StepExecutor) executeStoryStep(ctx context.Context, srun *runsv1alpha1. } var story bubuv1alpha1.Story - if err := e.Client.Get(ctx, with.StoryRef.ToNamespacedName(srun), &story); err != nil { + if err := e.Get(ctx, with.StoryRef.ToNamespacedName(srun), &story); err != nil { return fmt.Errorf("failed to get story '%s' for story step '%s': %w", with.StoryRef.Name, step.Name, err) } @@ -317,7 +317,7 @@ func (e *StepExecutor) mergeWithBlocks(engramWith, stepWith *runtime.RawExtensio return engramWith, nil } - var engramMap, stepMap map[string]interface{} + var engramMap, stepMap map[string]any if err := json.Unmarshal(engramWith.Raw, &engramMap); err != nil { return nil, fmt.Errorf("failed to unmarshal engram 'with' block: %w", err) } @@ -337,18 +337,18 @@ func (e *StepExecutor) mergeWithBlocks(engramWith, stepWith *runtime.RawExtensio return &runtime.RawExtension{Raw: mergedBytes}, nil } -func coerceToList(input interface{}) ([]interface{}, error) { +func coerceToList(input any) ([]any, error) { switch v := input.(type) { - case []interface{}: + case []any: return v, nil case []string: - var list []interface{} + var list []any for _, item := range v { list = append(list, item) } return list, nil case string: - return []interface{}{v}, nil + return []any{v}, nil default: return nil, fmt.Errorf("input '%v' is not a list or string", input) } diff --git a/internal/controller/runs/steprun_controller.go b/internal/controller/runs/steprun_controller.go index a239870..f2b8d73 100644 --- a/internal/controller/runs/steprun_controller.go +++ b/internal/controller/runs/steprun_controller.go @@ -58,18 +58,18 @@ type StepRunReconciler struct { Recorder record.EventRecorder } -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns/finalizers,verbs=update -//+kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch -//+kubebuilder:rbac:groups=bubustack.io,resources=engrams/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch -//+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch -//+kubebuilder:rbac:groups=core,resources=pods/log,verbs=get +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns/finalizers,verbs=update +// +kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch +// +kubebuilder:rbac:groups=bubustack.io,resources=engrams/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=pods/log,verbs=get func (r *StepRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { log := logging.NewReconcileLogger(ctx, "steprun").WithValues("steprun", req.NamespacedName) @@ -79,7 +79,7 @@ func (r *StepRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re }() // Bound reconcile duration - timeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout + timeout := r.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -93,7 +93,7 @@ func (r *StepRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re stepLogger := log.WithStepRun(&stepRun) - if !stepRun.ObjectMeta.DeletionTimestamp.IsZero() { + if !stepRun.DeletionTimestamp.IsZero() { return r.reconcileDelete(ctx, &stepRun) } @@ -101,7 +101,7 @@ func (r *StepRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re if !controllerutil.ContainsFinalizer(&stepRun, StepRunFinalizer) { beforePatch := stepRun.DeepCopy() controllerutil.AddFinalizer(&stepRun, StepRunFinalizer) - if err := r.ControllerDependencies.Client.Patch(ctx, &stepRun, client.MergeFrom(beforePatch)); err != nil { + if err := r.Patch(ctx, &stepRun, client.MergeFrom(beforePatch)); err != nil { stepLogger.Error(err, "Failed to add StepRun finalizer") return ctrl.Result{}, err } @@ -143,7 +143,7 @@ func (r *StepRunReconciler) reconcileNormal(ctx context.Context, step *runsv1alp // Default to "job" mode if not specified mode := enums.WorkloadModeJob - if engram != nil && engram.Spec.Mode != "" { //nolint:staticcheck + if engram != nil && engram.Spec.Mode != "" { mode = engram.Spec.Mode } @@ -175,7 +175,7 @@ func (r *StepRunReconciler) reconcileJobExecution(ctx context.Context, step *run stepLogger := logging.NewReconcileLogger(ctx, "steprun").WithStepRun(step) job := &batchv1.Job{} - err := r.ControllerDependencies.Client.Get(ctx, types.NamespacedName{Name: step.Name, Namespace: step.Namespace}, job) + err := r.Get(ctx, types.NamespacedName{Name: step.Name, Namespace: step.Namespace}, job) if err != nil { if errors.IsNotFound(err) { stepLogger.Info("Job not found, creating a new one") @@ -189,7 +189,7 @@ func (r *StepRunReconciler) reconcileJobExecution(ctx context.Context, step *run stepLogger.Error(err, "Failed to create Job for StepRun") return ctrl.Result{}, err } - if err := r.ControllerDependencies.Client.Create(ctx, newJob); err != nil { + if err := r.Create(ctx, newJob); err != nil { stepLogger.Error(err, "Failed to create Job in cluster") return ctrl.Result{}, err } @@ -218,7 +218,7 @@ func (r *StepRunReconciler) setStepRunPhase(ctx context.Context, step *runsv1alp } } - return patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, step, func(obj client.Object) { + return patch.RetryableStatusPatch(ctx, r.Client, step, func(obj client.Object) { s := obj.(*runsv1alpha1.StepRun) s.Status.Phase = phase s.Status.ObservedGeneration = s.Generation @@ -271,11 +271,11 @@ func (r *StepRunReconciler) updateEngramStatus(ctx context.Context, step *runsv1 engram := &v1alpha1.Engram{} engramKey := client.ObjectKey{Namespace: step.Namespace, Name: step.Spec.EngramRef.Name} - if err := r.ControllerDependencies.Client.Get(ctx, engramKey, engram); err != nil { + if err := r.Get(ctx, engramKey, engram); err != nil { return fmt.Errorf("failed to get parent engram %s for status update: %w", step.Spec.EngramRef.Name, err) } - return patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, engram, func(obj client.Object) { + return patch.RetryableStatusPatch(ctx, r.Client, engram, func(obj client.Object) { e := obj.(*v1alpha1.Engram) now := metav1.Now() e.Status.LastExecutionTime = &now @@ -292,7 +292,7 @@ func (r *StepRunReconciler) getEngramForStep(ctx context.Context, step *runsv1al } var engram v1alpha1.Engram key := step.Spec.EngramRef.ToNamespacedName(step) - if err := r.ControllerDependencies.Client.Get(ctx, key, &engram); err != nil { + if err := r.Get(ctx, key, &engram); err != nil { return nil, err } return &engram, nil @@ -302,7 +302,7 @@ func (r *StepRunReconciler) getEngramTemplateForEngram(ctx context.Context, engr var engramTemplate catalogv1alpha1.EngramTemplate // EngramTemplates are cluster-scoped, so we must not provide a namespace. key := client.ObjectKey{Name: engram.Spec.TemplateRef.Name} - if err := r.ControllerDependencies.Client.Get(ctx, key, &engramTemplate); err != nil { + if err := r.Get(ctx, key, &engramTemplate); err != nil { return nil, err } return &engramTemplate, nil @@ -317,7 +317,7 @@ func (r *StepRunReconciler) reconcileDelete(ctx context.Context, step *runsv1alp } job := &batchv1.Job{} - err := r.ControllerDependencies.Client.Get(ctx, types.NamespacedName{Name: step.Name, Namespace: step.Namespace}, job) + err := r.Get(ctx, types.NamespacedName{Name: step.Name, Namespace: step.Namespace}, job) if err != nil && !errors.IsNotFound(err) { stepLogger.Error(err, "Failed to get Job for cleanup check") @@ -327,7 +327,7 @@ func (r *StepRunReconciler) reconcileDelete(ctx context.Context, step *runsv1alp // If the job exists and is not being deleted, delete it. if err == nil && job.DeletionTimestamp.IsZero() { stepLogger.Info("Deleting owned Job") - if err := r.ControllerDependencies.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { + if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { stepLogger.Error(err, "Failed to delete Job during cleanup") return ctrl.Result{}, err } @@ -340,7 +340,7 @@ func (r *StepRunReconciler) reconcileDelete(ctx context.Context, step *runsv1alp stepLogger.Info("Owned Job is deleted, removing finalizer") beforePatch := step.DeepCopy() controllerutil.RemoveFinalizer(step, StepRunFinalizer) - if err := r.ControllerDependencies.Client.Patch(ctx, step, client.MergeFrom(beforePatch)); err != nil { + if err := r.Patch(ctx, step, client.MergeFrom(beforePatch)); err != nil { stepLogger.Error(err, "Failed to remove finalizer") return ctrl.Result{}, err } @@ -382,23 +382,23 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al } // Resolve inputs using the shared CEL evaluator - stepOutputs, err := getPriorStepOutputs(ctx, r.ControllerDependencies.Client, storyRun, nil) + stepOutputs, err := getPriorStepOutputs(ctx, r.Client, storyRun, nil) if err != nil { return nil, fmt.Errorf("failed to get prior step outputs: %w", err) } - var with map[string]interface{} + var with map[string]any if srun.Spec.Input != nil { if err := json.Unmarshal(srun.Spec.Input.Raw, &with); err != nil { return nil, fmt.Errorf("failed to unmarshal step 'with' block: %w", err) } } - vars := map[string]interface{}{ + vars := map[string]any{ "inputs": storyRunInputs, "steps": stepOutputs, } - resolvedInputs, err := r.ControllerDependencies.CELEvaluator.ResolveWithInputs(ctx, with, vars) + resolvedInputs, err := r.CELEvaluator.ResolveWithInputs(ctx, with, vars) if err != nil { // This is where we detect if an upstream output is not ready. // The CEL evaluator will return a compile error for an undeclared reference. @@ -411,10 +411,7 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al } // Setup secrets - secretEnvVars, volumes, volumeMounts, envFromSources, err := r.setupSecrets(ctx, resolvedConfig, engramTemplate) - if err != nil { - return nil, fmt.Errorf("failed to setup secrets for step '%s': %w", srun.Name, err) - } + secretEnvVars, volumes, volumeMounts := r.setupSecrets(ctx, resolvedConfig, engramTemplate) // Determine startedAt timestamp for accurate duration tracking startedAt := metav1.Now() @@ -432,13 +429,13 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al {Name: "BUBU_INPUTS", Value: string(inputBytes)}, {Name: "BUBU_STARTED_AT", Value: startedAt.Format(time.RFC3339Nano)}, {Name: "BUBU_EXECUTION_MODE", Value: "batch"}, - {Name: "BUBU_GRPC_PORT", Value: fmt.Sprintf("%d", r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort)}, - {Name: "BUBU_MAX_INLINE_SIZE", Value: fmt.Sprintf("%d", r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultMaxInlineSize)}, - {Name: "BUBU_STORAGE_TIMEOUT", Value: fmt.Sprintf("%ds", r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultStorageTimeoutSeconds)}, + {Name: "BUBU_GRPC_PORT", Value: fmt.Sprintf("%d", r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort)}, + {Name: "BUBU_MAX_INLINE_SIZE", Value: fmt.Sprintf("%d", r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultMaxInlineSize)}, + {Name: "BUBU_STORAGE_TIMEOUT", Value: fmt.Sprintf("%ds", r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultStorageTimeoutSeconds)}, } // Add gRPC tuning parameters for consistency, even in batch mode - engramConfig := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig + engramConfig := r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig envVars = append(envVars, corev1.EnvVar{Name: "BUBU_GRPC_MAX_RECV_BYTES", Value: fmt.Sprintf("%d", engramConfig.DefaultMaxRecvMsgBytes)}, corev1.EnvVar{Name: "BUBU_GRPC_MAX_SEND_BYTES", Value: fmt.Sprintf("%d", engramConfig.DefaultMaxSendMsgBytes)}, @@ -459,7 +456,7 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al // 2. Operator config DefaultStepTimeout // 3. Fallback to 30 minutes (matches SDK default) // This allows SDK to enforce timeout before Job-level activeDeadlineSeconds kills the pod - stepTimeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.DefaultStepTimeout + stepTimeout := r.ConfigResolver.GetOperatorConfig().Controller.DefaultStepTimeout if stepTimeout == 0 { stepTimeout = 30 * time.Minute // Fallback to SDK's default } @@ -522,7 +519,7 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al RestartPolicy: resolvedConfig.RestartPolicy, ServiceAccountName: resolvedConfig.ServiceAccountName, AutomountServiceAccountToken: &resolvedConfig.AutomountServiceAccountToken, - TerminationGracePeriodSeconds: &r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultTerminationGracePeriodSeconds, + TerminationGracePeriodSeconds: &r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultTerminationGracePeriodSeconds, SecurityContext: resolvedConfig.ToPodSecurityContext(), Containers: []corev1.Container{{ Name: "engram", @@ -560,11 +557,11 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al if auth := &s3Config.Authentication; auth.SecretRef != nil { secretName := auth.SecretRef.Name stepLogger.Info("Using S3 secret reference for authentication", "secretName", secretName) - envFromSources = append(envFromSources, corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) + // envFromSources = append(envFromSources, corev1.EnvFromSource{ // This line is removed + // SecretRef: &corev1.SecretEnvSource{ + // LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + // }, + // }) } } @@ -572,7 +569,7 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al job.Spec.Template.Spec.Volumes = volumes job.Spec.Template.Spec.Containers[0].VolumeMounts = volumeMounts job.Spec.Template.Spec.Containers[0].Env = envVars - job.Spec.Template.Spec.Containers[0].EnvFrom = envFromSources + // job.Spec.Template.Spec.Containers[0].EnvFrom = envFromSources // This line is removed if err := controllerutil.SetControllerReference(srun, job, r.Scheme); err != nil { return nil, err @@ -582,12 +579,12 @@ func (r *StepRunReconciler) createJobForStep(ctx context.Context, srun *runsv1al } // getStoryRunInputs fetches the initial inputs from the parent StoryRun. -func (r *StepRunReconciler) getStoryRunInputs(ctx context.Context, storyRun *runsv1alpha1.StoryRun) (map[string]interface{}, error) { +func (r *StepRunReconciler) getStoryRunInputs(_ context.Context, storyRun *runsv1alpha1.StoryRun) (map[string]any, error) { if storyRun.Spec.Inputs == nil { - return make(map[string]interface{}), nil + return make(map[string]any), nil } - var inputs map[string]interface{} + var inputs map[string]any if err := json.Unmarshal(storyRun.Spec.Inputs.Raw, &inputs); err != nil { return nil, fmt.Errorf("failed to unmarshal storyrun inputs: %w", err) } @@ -597,7 +594,7 @@ func (r *StepRunReconciler) getStoryRunInputs(ctx context.Context, storyRun *run func (r *StepRunReconciler) getParentStoryRun(ctx context.Context, srun *runsv1alpha1.StepRun) (*runsv1alpha1.StoryRun, error) { var storyRun runsv1alpha1.StoryRun key := types.NamespacedName{Name: srun.Spec.StoryRunRef.Name, Namespace: srun.Namespace} - if err := r.ControllerDependencies.Client.Get(ctx, key, &storyRun); err != nil { + if err := r.Get(ctx, key, &storyRun); err != nil { return nil, err } return &storyRun, nil @@ -638,7 +635,7 @@ func (r *StepRunReconciler) handleJobStatus(ctx context.Context, step *runsv1alp // Use the original reconcile context. If it has timed out, the patch will fail // and the entire reconcile will be retried. This ensures the exit code is // eventually recorded without risking a race condition from a detached context. - if err := patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, step, func(obj client.Object) { + if err := patch.RetryableStatusPatch(ctx, r.Client, step, func(obj client.Object) { sr := obj.(*runsv1alpha1.StepRun) sr.Status.ExitCode = int32(exitCode) // Optionally set ExitClass for semantic categorization @@ -657,7 +654,7 @@ func (r *StepRunReconciler) handleJobStatus(ctx context.Context, step *runsv1alp // Use the original reconcile context. If it has timed out, the patch will fail and // the entire reconcile will be retried, which is the correct and safe behavior for // ensuring this terminal state is eventually recorded. - if err := patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, step, func(obj client.Object) { + if err := patch.RetryableStatusPatch(ctx, r.Client, step, func(obj client.Object) { sr := obj.(*runsv1alpha1.StepRun) sr.Status.Phase = enums.PhaseFailed sr.Status.LastFailureMsg = "Job execution failed. Check pod logs for details." @@ -685,7 +682,7 @@ func (r *StepRunReconciler) handleJobStatus(ctx context.Context, step *runsv1alp func (r *StepRunReconciler) extractPodExitCode(ctx context.Context, job *batchv1.Job) int { // List pods owned by this Job var podList corev1.PodList - if err := r.ControllerDependencies.Client.List(ctx, &podList, client.InNamespace(job.Namespace), client.MatchingLabels{"job-name": job.Name}); err != nil { + if err := r.List(ctx, &podList, client.InNamespace(job.Namespace), client.MatchingLabels{"job-name": job.Name}); err != nil { return 0 } @@ -742,12 +739,12 @@ func classifyExitCode(code int) enums.ExitClass { func (r *StepRunReconciler) getStoryForStep(ctx context.Context, step *runsv1alpha1.StepRun) (*v1alpha1.Story, error) { storyRun := &runsv1alpha1.StoryRun{} storyRunKey := types.NamespacedName{Name: step.Spec.StoryRunRef.Name, Namespace: step.Namespace} - if err := r.ControllerDependencies.Client.Get(ctx, storyRunKey, storyRun); err != nil { + if err := r.Get(ctx, storyRunKey, storyRun); err != nil { return nil, fmt.Errorf("failed to get parent StoryRun %s: %w", step.Spec.StoryRunRef.Name, err) } story := &v1alpha1.Story{} storyKey := storyRun.Spec.StoryRef.ToNamespacedName(storyRun) - if err := r.ControllerDependencies.Client.Get(ctx, storyKey, story); err != nil { + if err := r.Get(ctx, storyKey, story); err != nil { return nil, fmt.Errorf("failed to get parent Story %s: %w", storyRun.Spec.StoryRef.Name, err) } return story, nil @@ -769,7 +766,7 @@ func (r *StepRunReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.O func (r *StepRunReconciler) mapEngramToStepRuns(ctx context.Context, obj client.Object) []reconcile.Request { log := logging.NewReconcileLogger(ctx, "steprun-mapper").WithValues("engram", obj.GetName()) var stepRuns runsv1alpha1.StepRunList - if err := r.ControllerDependencies.Client.List(ctx, &stepRuns, client.InNamespace(obj.GetNamespace()), client.MatchingFields{"spec.engramRef": obj.GetName()}); err != nil { + if err := r.List(ctx, &stepRuns, client.InNamespace(obj.GetNamespace()), client.MatchingFields{"spec.engramRef": obj.GetName()}); err != nil { log.Error(err, "failed to list stepruns for engram") return nil } @@ -795,7 +792,7 @@ func (r *StepRunReconciler) mapEngramTemplateToStepRuns(ctx context.Context, obj // 1. Find all Engrams that reference this EngramTemplate. var engrams v1alpha1.EngramList // Note: We list across all namespaces because Engrams in any namespace can reference a cluster-scoped template. - if err := r.ControllerDependencies.Client.List(ctx, &engrams, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { + if err := r.List(ctx, &engrams, client.MatchingFields{"spec.templateRef.name": obj.GetName()}); err != nil { log.Error(err, "failed to list engrams for engramtemplate") return nil } @@ -808,7 +805,7 @@ func (r *StepRunReconciler) mapEngramTemplateToStepRuns(ctx context.Context, obj var allRequests []reconcile.Request for _, engram := range engrams.Items { var stepRuns runsv1alpha1.StepRunList - if err := r.ControllerDependencies.Client.List(ctx, &stepRuns, client.InNamespace(engram.GetNamespace()), client.MatchingFields{"spec.engramRef": engram.GetName()}); err != nil { + if err := r.List(ctx, &stepRuns, client.InNamespace(engram.GetNamespace()), client.MatchingFields{"spec.engramRef": engram.GetName()}); err != nil { log.Error(err, "failed to list stepruns for engram", "engram", engram.GetName()) continue // Continue to the next engram } @@ -831,14 +828,14 @@ func (r *StepRunReconciler) mapEngramTemplateToStepRuns(ctx context.Context, obj // setupSecrets resolves the secret mappings from the Engram and prepares the necessary // volumes, volume mounts, and environment variables for the pod. -func (r *StepRunReconciler) setupSecrets(ctx context.Context, resolvedConfig *config.ResolvedExecutionConfig, engramTemplate *catalogv1alpha1.EngramTemplate) ([]corev1.EnvVar, []corev1.Volume, []corev1.VolumeMount, []corev1.EnvFromSource, error) { +func (r *StepRunReconciler) setupSecrets(_ context.Context, resolvedConfig *config.ResolvedExecutionConfig, engramTemplate *catalogv1alpha1.EngramTemplate) ([]corev1.EnvVar, []corev1.Volume, []corev1.VolumeMount) { var envVars []corev1.EnvVar var volumes []corev1.Volume var volumeMounts []corev1.VolumeMount - var envFromSources []corev1.EnvFromSource + // Note: we no longer return EnvFromSource; secrets are either mounted as files or injected via explicit env vars if resolvedConfig.Secrets == nil || engramTemplate.Spec.SecretSchema == nil { - return envVars, volumes, volumeMounts, envFromSources, nil + return envVars, volumes, volumeMounts } for logicalName, actualSecretName := range resolvedConfig.Secrets { @@ -915,5 +912,5 @@ func (r *StepRunReconciler) setupSecrets(ctx context.Context, resolvedConfig *co } } - return envVars, volumes, volumeMounts, envFromSources, nil + return envVars, volumes, volumeMounts } diff --git a/internal/controller/runs/storyrun_controller.go b/internal/controller/runs/storyrun_controller.go index aad5c1c..73eda16 100644 --- a/internal/controller/runs/storyrun_controller.go +++ b/internal/controller/runs/storyrun_controller.go @@ -56,15 +56,15 @@ type StoryRunReconciler struct { Recorder record.EventRecorder } -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns/finalizers,verbs=update -//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;get;watch;list -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;watch;list -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;get;watch;list -//+kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns,verbs=get;list;watch;create;update;patch -//+kubebuilder:rbac:groups=bubustack.io,resources=stories,verbs=get;list;watch -//+kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=storyruns/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;get;watch;list +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;get;watch;list +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;get;watch;list +// +kubebuilder:rbac:groups=runs.bubustack.io,resources=stepruns,verbs=get;list;watch;create;update;patch +// +kubebuilder:rbac:groups=bubustack.io,resources=stories,verbs=get;list;watch +// +kubebuilder:rbac:groups=bubustack.io,resources=engrams,verbs=get;list;watch func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { log := logging.NewReconcileLogger(ctx, "storyrun").WithValues("storyrun", req.NamespacedName) @@ -74,7 +74,7 @@ func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r }() // Bound reconcile duration - timeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout + timeout := r.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -82,7 +82,7 @@ func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r } var srun runsv1alpha1.StoryRun - if err := r.ControllerDependencies.Client.Get(ctx, req.NamespacedName, &srun); err != nil { + if err := r.Get(ctx, req.NamespacedName, &srun); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -137,7 +137,7 @@ func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r if r.Recorder != nil { r.Recorder.Event(&srun, "Warning", conditions.ReasonStoryNotFound, fmt.Sprintf("Waiting for Story '%s'", srun.Spec.StoryRef.ToNamespacedName(&srun).String())) } - statusErr := patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, &srun, func(obj client.Object) { + statusErr := patch.RetryableStatusPatch(ctx, r.Client, &srun, func(obj client.Object) { sr := obj.(*runsv1alpha1.StoryRun) if sr.Status.Phase == "" { sr.Status.Phase = enums.PhasePending @@ -164,7 +164,7 @@ func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r } if story.Spec.Pattern == enums.StreamingPattern { - if _, err := r.reconcileStreamingStoryRun(ctx, &srun, story); err != nil { + if err := r.reconcileStreamingStoryRun(ctx, &srun, story); err != nil { if ctx.Err() == context.DeadlineExceeded { log.Info("Reconciliation timed out, returning error to trigger failure rate limiter") return ctrl.Result{}, fmt.Errorf("reconcile timed out: %w", ctx.Err()) @@ -191,7 +191,7 @@ func (r *StoryRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r return r.dagReconciler.Reconcile(ctx, &srun, story) } -func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubushv1alpha1.Story) (ctrl.Result, error) { +func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, srun *runsv1alpha1.StoryRun, story *bubushv1alpha1.Story) error { log := logging.NewReconcileLogger(ctx, "storyrun").WithValues("storyrun", srun.Name) if story.Spec.StreamingStrategy == enums.StreamingStrategyPerStoryRun { @@ -206,11 +206,11 @@ func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, sru // Get the original engram definition originalEngram := &bubushv1alpha1.Engram{} originalEngramKey := step.Ref.ToNamespacedName(story) - if err := r.ControllerDependencies.Client.Get(ctx, originalEngramKey, originalEngram); err != nil { + if err := r.Get(ctx, originalEngramKey, originalEngram); err != nil { if errors.IsNotFound(err) { - return ctrl.Result{}, fmt.Errorf("step '%s' references engram '%s' which does not exist", step.Name, originalEngram.Name) + return fmt.Errorf("step '%s' references engram '%s' which does not exist", step.Name, originalEngram.Name) } - return ctrl.Result{}, fmt.Errorf("failed to get engram for step '%s': %w", step.Name, err) + return fmt.Errorf("failed to get engram for step '%s': %w", step.Name, err) } // Create or update the run-specific engram @@ -221,7 +221,7 @@ func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, sru }, } - op, err := controllerutil.CreateOrUpdate(ctx, r.ControllerDependencies.Client, runEngram, func() error { + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, runEngram, func() error { // Copy the spec from the original engram runEngram.Spec = *originalEngram.Spec.DeepCopy() // Ensure the mode is a long-running one @@ -230,12 +230,12 @@ func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, sru runEngram.Spec.Mode = enums.WorkloadModeDeployment } // Set the StoryRun as the owner - return controllerutil.SetControllerReference(srun, runEngram, r.ControllerDependencies.Scheme) + return controllerutil.SetControllerReference(srun, runEngram, r.Scheme) }) if err != nil { log.Error(err, "Failed to create or update run-specific Engram", "step", step.Name) - return ctrl.Result{}, err + return err } if op != controllerutil.OperationResultNone { log.Info("Reconciled run-specific engram", "engram", runEngram.Name, "operation", op) @@ -247,11 +247,11 @@ func (r *StoryRunReconciler) reconcileStreamingStoryRun(ctx context.Context, sru // We can now mark the StoryRun as running. if srun.Status.Phase == enums.PhasePending { if err := r.setStoryRunPhase(ctx, srun, enums.PhaseRunning, "Streaming StoryRun is active"); err != nil { - return ctrl.Result{}, err + return err } } - return ctrl.Result{}, nil + return nil } func (r *StoryRunReconciler) reconcileDelete(ctx context.Context, srun *runsv1alpha1.StoryRun) (ctrl.Result, error) { @@ -260,7 +260,7 @@ func (r *StoryRunReconciler) reconcileDelete(ctx context.Context, srun *runsv1al // List all StepRuns for this StoryRun var stepRunList runsv1alpha1.StepRunList - if err := r.ControllerDependencies.Client.List(ctx, &stepRunList, client.InNamespace(srun.Namespace), client.MatchingLabels{"bubustack.io/storyrun": srun.Name}); err != nil { + if err := r.List(ctx, &stepRunList, client.InNamespace(srun.Namespace), client.MatchingLabels{"bubustack.io/storyrun": srun.Name}); err != nil { log.Error(err, "Failed to list StepRuns for cleanup") return ctrl.Result{}, err } @@ -280,11 +280,11 @@ func (r *StoryRunReconciler) reconcileDelete(ctx context.Context, srun *runsv1al // Once all children are gone, remove the finalizer. if len(stepRunList.Items) == 0 { if controllerutil.ContainsFinalizer(srun, StoryRunFinalizer) { - patch := client.MergeFrom(srun.DeepCopy()) + mergePatch := client.MergeFrom(srun.DeepCopy()) controllerutil.RemoveFinalizer(srun, StoryRunFinalizer) // Use Patch for atomicity, avoiding race conditions with other updaters. // This is safer than Update for modifying metadata like finalizers. - if err := r.Patch(ctx, srun, patch); err != nil { + if err := r.Patch(ctx, srun, mergePatch); err != nil { log.Error(err, "Failed to remove finalizer") return ctrl.Result{}, err } @@ -301,14 +301,14 @@ func (r *StoryRunReconciler) reconcileDelete(ctx context.Context, srun *runsv1al func (r *StoryRunReconciler) getStoryForRun(ctx context.Context, srun *runsv1alpha1.StoryRun) (*bubushv1alpha1.Story, error) { var story bubushv1alpha1.Story key := srun.Spec.StoryRef.ToNamespacedName(srun) - if err := r.ControllerDependencies.Client.Get(ctx, key, &story); err != nil { + if err := r.Get(ctx, key, &story); err != nil { return nil, err } return &story, nil } func (r *StoryRunReconciler) setStoryRunPhase(ctx context.Context, srun *runsv1alpha1.StoryRun, phase enums.Phase, message string) error { - return patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, srun, func(obj client.Object) { + return patch.RetryableStatusPatch(ctx, r.Client, srun, func(obj client.Object) { sr := obj.(*runsv1alpha1.StoryRun) sr.Status.Phase = phase sr.Status.Message = message @@ -352,8 +352,8 @@ func (r *StoryRunReconciler) setStoryRunPhase(ctx context.Context, srun *runsv1a // SetupWithManager sets up the controller with the Manager. func (r *StoryRunReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error { r.rbacManager = NewRBACManager(mgr.GetClient(), mgr.GetScheme()) - stepExecutor := NewStepExecutor(mgr.GetClient(), mgr.GetScheme(), &r.ControllerDependencies.CELEvaluator) - r.dagReconciler = NewDAGReconciler(mgr.GetClient(), &r.ControllerDependencies.CELEvaluator, stepExecutor, r.ControllerDependencies.ConfigResolver) + stepExecutor := NewStepExecutor(mgr.GetClient(), mgr.GetScheme(), &r.CELEvaluator) + r.dagReconciler = NewDAGReconciler(mgr.GetClient(), &r.CELEvaluator, stepExecutor, r.ConfigResolver) r.Recorder = mgr.GetEventRecorderFor("storyrun-controller") return ctrl.NewControllerManagedBy(mgr). diff --git a/internal/controller/story_controller.go b/internal/controller/story_controller.go index cb4e489..dd49cfe 100644 --- a/internal/controller/story_controller.go +++ b/internal/controller/story_controller.go @@ -73,14 +73,14 @@ type StoryReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. func (r *StoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) startTime := time.Now() defer func() { metrics.RecordControllerReconcile("story", time.Since(startTime), err) }() // Bound reconcile duration - timeout := r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout + timeout := r.ConfigResolver.GetOperatorConfig().Controller.ReconcileTimeout if timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -88,21 +88,24 @@ func (r *StoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res } var story bubuv1alpha1.Story - if err := r.ControllerDependencies.Client.Get(ctx, req.NamespacedName, &story); err != nil { + if err := r.Get(ctx, req.NamespacedName, &story); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Handle deletion first. - if !story.ObjectMeta.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, &story) + if !story.DeletionTimestamp.IsZero() { + if err := r.reconcileDelete(ctx, &story); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } // Add finalizer if it doesn't exist. if !controllerutil.ContainsFinalizer(&story, StoryFinalizer) { - patch := client.MergeFrom(story.DeepCopy()) + mergePatch := client.MergeFrom(story.DeepCopy()) controllerutil.AddFinalizer(&story, StoryFinalizer) - if err := r.Patch(ctx, &story, patch); err != nil { - log.Error(err, "Failed to add finalizer to Story") + if err := r.Patch(ctx, &story, mergePatch); err != nil { + logger.Error(err, "Failed to add finalizer to Story") return ctrl.Result{}, err } } @@ -117,7 +120,7 @@ func (r *StoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res } // Update the status based on validation. - err = patch.RetryableStatusPatch(ctx, r.ControllerDependencies.Client, &story, func(obj client.Object) { + err = patch.RetryableStatusPatch(ctx, r.Client, &story, func(obj client.Object) { s := obj.(*bubuv1alpha1.Story) s.Status.StepsTotal = int32(len(s.Spec.Steps)) s.Status.ObservedGeneration = s.Generation @@ -141,36 +144,39 @@ func (r *StoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res // Handle streaming pattern infrastructure for PerStory strategy if story.Spec.Pattern == enums.StreamingPattern && story.Spec.StreamingStrategy == enums.StreamingStrategyPerStory { - return r.reconcilePerStoryStreaming(ctx, &story) + if err := r.reconcilePerStoryStreaming(ctx, &story); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } return ctrl.Result{}, nil } -func (r *StoryReconciler) reconcileDelete(ctx context.Context, story *bubuv1alpha1.Story) (ctrl.Result, error) { - log := log.FromContext(ctx) - log.Info("Reconciling deletion for Story") +func (r *StoryReconciler) reconcileDelete(ctx context.Context, story *bubuv1alpha1.Story) error { + logger := log.FromContext(ctx) + logger.Info("Reconciling deletion for Story") if controllerutil.ContainsFinalizer(story, StoryFinalizer) { // Clean up external resources associated with the Story, such as Deployments and Services. if err := r.cleanupOwnedResources(ctx, story); err != nil { - return ctrl.Result{}, err + return err } // Remove the finalizer from the list and update it. - patch := client.MergeFrom(story.DeepCopy()) + mergePatch := client.MergeFrom(story.DeepCopy()) controllerutil.RemoveFinalizer(story, StoryFinalizer) - if err := r.Patch(ctx, story, patch); err != nil { - log.Error(err, "Failed to remove finalizer from Story") - return ctrl.Result{}, err + if err := r.Patch(ctx, story, mergePatch); err != nil { + logger.Error(err, "Failed to remove finalizer from Story") + return err } } - return ctrl.Result{}, nil + return nil } func (r *StoryReconciler) cleanupOwnedResources(ctx context.Context, story *bubuv1alpha1.Story) error { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) // This cleanup logic is only for streaming stories with a PerStory strategy. if story.Spec.Pattern != enums.StreamingPattern || story.Spec.StreamingStrategy != enums.StreamingStrategyPerStory { @@ -188,7 +194,7 @@ func (r *StoryReconciler) cleanupOwnedResources(ctx context.Context, story *bubu }, } if err := r.Delete(ctx, deployment, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) { - log.Error(err, "Failed to delete Deployment for streaming step", "deployment", deploymentName) + logger.Error(err, "Failed to delete Deployment for streaming step", "deployment", deploymentName) return err } @@ -201,19 +207,19 @@ func (r *StoryReconciler) cleanupOwnedResources(ctx context.Context, story *bubu }, } if err := r.Delete(ctx, service); err != nil && !errors.IsNotFound(err) { - log.Error(err, "Failed to delete Service for streaming step", "service", serviceName) + logger.Error(err, "Failed to delete Service for streaming step", "service", serviceName) return err } } } - log.Info("Successfully cleaned up owned resources") + logger.Info("Successfully cleaned up owned resources") return nil } -func (r *StoryReconciler) reconcilePerStoryStreaming(ctx context.Context, story *bubuv1alpha1.Story) (ctrl.Result, error) { - log := log.FromContext(ctx) - log.Info("Reconciling workloads for streaming Story with PerStory strategy") +func (r *StoryReconciler) reconcilePerStoryStreaming(ctx context.Context, story *bubuv1alpha1.Story) error { + logger := log.FromContext(ctx) + logger.Info("Reconciling workloads for streaming Story with PerStory strategy") for _, step := range story.Spec.Steps { if step.Ref == nil { @@ -223,46 +229,46 @@ func (r *StoryReconciler) reconcilePerStoryStreaming(ctx context.Context, story // Fetch the Engram to get its spec var engram bubuv1alpha1.Engram engramKey := step.Ref.ToNamespacedName(story) - if err := r.ControllerDependencies.Client.Get(ctx, engramKey, &engram); err != nil { - log.Error(err, "Failed to get Engram for streaming step", "engram", engramKey.Name) - return ctrl.Result{}, err // Requeue and try again + if err := r.Get(ctx, engramKey, &engram); err != nil { + logger.Error(err, "Failed to get Engram for streaming step", "engram", engramKey.Name) + return err // Requeue and try again } // Resolve template and execution config to build a real deployment // Fetch EngramTemplate template := &catalogv1alpha1.EngramTemplate{} - if err := r.ControllerDependencies.Client.Get(ctx, types.NamespacedName{Name: engram.Spec.TemplateRef.Name, Namespace: ""}, template); err != nil { - log.Error(err, "Failed to get EngramTemplate for streaming step", "engramTemplate", engram.Spec.TemplateRef.Name) - return ctrl.Result{}, err + if err := r.Get(ctx, types.NamespacedName{Name: engram.Spec.TemplateRef.Name, Namespace: ""}, template); err != nil { + logger.Error(err, "Failed to get EngramTemplate for streaming step", "engramTemplate", engram.Spec.TemplateRef.Name) + return err } - resolved, err := r.ControllerDependencies.ConfigResolver.ResolveExecutionConfig(ctx, nil, story, &engram, template) + resolved, err := r.ConfigResolver.ResolveExecutionConfig(ctx, nil, story, &engram, template) if err != nil { - log.Error(err, "Failed to resolve execution config for streaming step") - return ctrl.Result{}, err + logger.Error(err, "Failed to resolve execution config for streaming step") + return err } deployment := r.deploymentForStreamingStepWithConfig(story, &step, &engram, resolved) if err := r.reconcileOwnedDeployment(ctx, story, deployment); err != nil { - return ctrl.Result{}, err + return err } service := r.serviceForStreamingStepWithConfig(story, &step, &engram, resolved) if err := r.reconcileOwnedService(ctx, story, service); err != nil { - return ctrl.Result{}, err + return err } } - return ctrl.Result{}, nil + return nil } func (r *StoryReconciler) reconcileOwnedDeployment(ctx context.Context, owner *bubuv1alpha1.Story, desired *appsv1.Deployment) error { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) existing := &appsv1.Deployment{} err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing) if err != nil && errors.IsNotFound(err) { if err := controllerutil.SetControllerReference(owner, desired, r.Scheme); err != nil { return err } - log.Info("Creating Deployment for streaming Story step", "deployment", desired.Name) + logger.Info("Creating Deployment for streaming Story step", "deployment", desired.Name) return r.Create(ctx, desired) } else if err != nil { return err @@ -271,24 +277,24 @@ func (r *StoryReconciler) reconcileOwnedDeployment(ctx context.Context, owner *b // Preserve cluster-managed fields via fresh GET + merge if !reflect.DeepEqual(existing.Spec.Template, desired.Spec.Template) || (existing.Spec.Replicas != nil && desired.Spec.Replicas != nil && *existing.Spec.Replicas != *desired.Spec.Replicas) { - patch := client.MergeFrom(existing.DeepCopy()) + mergePatch := client.MergeFrom(existing.DeepCopy()) existing.Spec.Template = desired.Spec.Template existing.Spec.Replicas = desired.Spec.Replicas - log.Info("Patching Deployment for streaming Story step", "deployment", desired.Name) - return r.Patch(ctx, existing, patch) + logger.Info("Patching Deployment for streaming Story step", "deployment", desired.Name) + return r.Patch(ctx, existing, mergePatch) } return nil } func (r *StoryReconciler) reconcileOwnedService(ctx context.Context, owner *bubuv1alpha1.Story, desired *corev1.Service) error { - log := log.FromContext(ctx) + logger := log.FromContext(ctx) existing := &corev1.Service{} err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing) if err != nil && errors.IsNotFound(err) { if err := controllerutil.SetControllerReference(owner, desired, r.Scheme); err != nil { return err } - log.Info("Creating Service for streaming Story step", "service", desired.Name) + logger.Info("Creating Service for streaming Story step", "service", desired.Name) return r.Create(ctx, desired) } else if err != nil { return err @@ -299,13 +305,13 @@ func (r *StoryReconciler) reconcileOwnedService(ctx context.Context, owner *bubu original := existing.DeepCopy() needsPatch := false - if !reflect.DeepEqual(existing.ObjectMeta.Labels, desired.ObjectMeta.Labels) { - existing.ObjectMeta.Labels = desired.ObjectMeta.Labels + if !reflect.DeepEqual(existing.Labels, desired.Labels) { + existing.Labels = desired.Labels needsPatch = true } - if !reflect.DeepEqual(existing.ObjectMeta.Annotations, desired.ObjectMeta.Annotations) { - existing.ObjectMeta.Annotations = desired.ObjectMeta.Annotations + if !reflect.DeepEqual(existing.Annotations, desired.Annotations) { + existing.Annotations = desired.Annotations needsPatch = true } @@ -320,14 +326,14 @@ func (r *StoryReconciler) reconcileOwnedService(ctx context.Context, owner *bubu } if needsPatch { - log.Info("Patching Service for streaming Story step", "service", desired.Name) + logger.Info("Patching Service for streaming Story step", "service", desired.Name) return r.Patch(ctx, existing, client.MergeFrom(original)) } return nil } -func (r *StoryReconciler) deploymentForStreamingStepWithConfig(story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, engram *bubuv1alpha1.Engram, cfg *config.ResolvedExecutionConfig) *appsv1.Deployment { +func (r *StoryReconciler) deploymentForStreamingStepWithConfig(story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, _ *bubuv1alpha1.Engram, cfg *config.ResolvedExecutionConfig) *appsv1.Deployment { name := fmt.Sprintf("%s-%s", story.Name, step.Name) labels := map[string]string{ "app.kubernetes.io/name": "bobrapet-streaming-engram", @@ -358,7 +364,7 @@ func (r *StoryReconciler) deploymentForStreamingStepWithConfig(story *bubuv1alph StartupProbe: cfg.StartupProbe, Ports: []corev1.ContainerPort{{ Name: "grpc", - ContainerPort: int32(r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort), + ContainerPort: int32(r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort), }}, Env: []corev1.EnvVar{{Name: "BUBU_EXECUTION_MODE", Value: "streaming"}}, }}, @@ -368,7 +374,7 @@ func (r *StoryReconciler) deploymentForStreamingStepWithConfig(story *bubuv1alph } } -func (r *StoryReconciler) serviceForStreamingStepWithConfig(story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, engram *bubuv1alpha1.Engram, cfg *config.ResolvedExecutionConfig) *corev1.Service { +func (r *StoryReconciler) serviceForStreamingStepWithConfig(story *bubuv1alpha1.Story, step *bubuv1alpha1.Step, _ *bubuv1alpha1.Engram, _ *config.ResolvedExecutionConfig) *corev1.Service { name := fmt.Sprintf("%s-%s", story.Name, step.Name) labels := map[string]string{ "bubustack.io/story": story.Name, @@ -383,7 +389,7 @@ func (r *StoryReconciler) serviceForStreamingStepWithConfig(story *bubuv1alpha1. Selector: labels, Ports: []corev1.ServicePort{{ Protocol: corev1.ProtocolTCP, - Port: int32(r.ControllerDependencies.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort), + Port: int32(r.ConfigResolver.GetOperatorConfig().Controller.Engram.EngramControllerConfig.DefaultGRPCPort), TargetPort: intstr.FromString("grpc"), }}, }, @@ -395,7 +401,7 @@ func (r *StoryReconciler) validateEngramReferences(ctx context.Context, story *b if step.Ref != nil { // This is an Engram step. var engram bubuv1alpha1.Engram key := step.Ref.ToNamespacedName(story) - if err := r.ControllerDependencies.Client.Get(ctx, key, &engram); err != nil { + if err := r.Get(ctx, key, &engram); err != nil { if errors.IsNotFound(err) { return fmt.Errorf("step '%s' references engram '%s' which does not exist in namespace '%s'", step.Name, key.Name, key.Namespace) } @@ -419,7 +425,7 @@ func (r *StoryReconciler) validateEngramReferences(ctx context.Context, story *b } var subStory bubuv1alpha1.Story key := with.StoryRef.ToNamespacedName(story) - if err := r.ControllerDependencies.Client.Get(ctx, key, &subStory); err != nil { + if err := r.Get(ctx, key, &subStory); err != nil { if errors.IsNotFound(err) { return fmt.Errorf("step '%s' references story '%s' which does not exist in namespace '%s'", step.Name, key.Name, key.Namespace) } @@ -441,7 +447,7 @@ func (r *StoryReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Opt r.Recorder = mgr.GetEventRecorderFor("story-controller") mapEngramToStories := func(ctx context.Context, obj client.Object) []reconcile.Request { var stories bubuv1alpha1.StoryList - if err := r.Client.List(ctx, &stories, client.InNamespace(obj.GetNamespace()), client.MatchingFields{"spec.steps.ref.name": obj.GetName()}); err != nil { + if err := r.List(ctx, &stories, client.InNamespace(obj.GetNamespace()), client.MatchingFields{"spec.steps.ref.name": obj.GetName()}); err != nil { return nil } reqs := make([]reconcile.Request, 0, len(stories.Items)) diff --git a/internal/webhook/v1alpha1/impulse_webhook.go b/internal/webhook/v1alpha1/impulse_webhook.go index 3868ab6..6c70be6 100644 --- a/internal/webhook/v1alpha1/impulse_webhook.go +++ b/internal/webhook/v1alpha1/impulse_webhook.go @@ -139,88 +139,119 @@ func (v *ImpulseCustomValidator) ValidateDelete(ctx context.Context, obj runtime // - with and mapping, if present, must be JSON objects (not array/primitive) // - workload.mode must not be 'job' (impulse must be always-on) func (v *ImpulseCustomValidator) validateImpulse(ctx context.Context, impulse *bubushv1alpha1.Impulse) error { + if err := v.validateRequiredFields(impulse); err != nil { + return err + } + template, err := v.fetchTemplate(ctx, impulse.Spec.TemplateRef.Name) + if err != nil { + return err + } + if err := v.validateWithBlock(impulse, template); err != nil { + return err + } + if err := v.validateMappingBlock(impulse, template); err != nil { + return err + } + if err := v.validateWorkloadMode(impulse); err != nil { + return err + } + return nil +} + +func (v *ImpulseCustomValidator) validateRequiredFields(impulse *bubushv1alpha1.Impulse) error { if impulse.Spec.TemplateRef.Name == "" { return fmt.Errorf("spec.templateRef.name is required") } if impulse.Spec.StoryRef.Name == "" { return fmt.Errorf("spec.storyRef.name is required") } + return nil +} - // Fetch the template to validate against its schemas +func (v *ImpulseCustomValidator) fetchTemplate(ctx context.Context, name string) (*v1alpha1.ImpulseTemplate, error) { var template v1alpha1.ImpulseTemplate - if err := v.Client.Get(ctx, types.NamespacedName{Name: impulse.Spec.TemplateRef.Name, Namespace: ""}, &template); err != nil { + if err := v.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: ""}, &template); err != nil { if errors.IsNotFound(err) { - return fmt.Errorf("ImpulseTemplate '%s' not found", impulse.Spec.TemplateRef.Name) + return nil, fmt.Errorf("ImpulseTemplate '%s' not found", name) } - return fmt.Errorf("failed to get ImpulseTemplate '%s': %w", impulse.Spec.TemplateRef.Name, err) + return nil, fmt.Errorf("failed to get ImpulseTemplate '%s': %w", name, err) } + return &template, nil +} - if impulse.Spec.With != nil && len(impulse.Spec.With.Raw) > 0 { - b := impulse.Spec.With.Raw - for len(b) > 0 && (b[0] == ' ' || b[0] == '\n' || b[0] == '\t' || b[0] == '\r') { - b = b[1:] - } - if len(b) > 0 && b[0] != '{' { - return fmt.Errorf("spec.with must be a JSON object") - } - // Inline size cap aligned with operator configuration - maxBytes := v.Config.Engram.EngramControllerConfig.DefaultMaxInlineSize - if maxBytes == 0 { - maxBytes = 1024 - } - if len(impulse.Spec.With.Raw) > maxBytes { - return fmt.Errorf("spec.with too large (%d bytes). Provide large payloads via object storage and references instead of inlining", len(impulse.Spec.With.Raw)) +func (v *ImpulseCustomValidator) validateWithBlock(impulse *bubushv1alpha1.Impulse, template *v1alpha1.ImpulseTemplate) error { + if impulse.Spec.With == nil || len(impulse.Spec.With.Raw) == 0 { + return nil + } + b := impulse.Spec.With.Raw + for len(b) > 0 && (b[0] == ' ' || b[0] == '\n' || b[0] == '\t' || b[0] == '\r') { + b = b[1:] + } + if len(b) > 0 && b[0] != '{' { + return fmt.Errorf("spec.with must be a JSON object") + } + maxBytes := v.Config.Engram.EngramControllerConfig.DefaultMaxInlineSize + if maxBytes == 0 { + maxBytes = 1024 + } + if len(impulse.Spec.With.Raw) > maxBytes { + return fmt.Errorf("spec.with too large (%d bytes). Provide large payloads via object storage and references instead of inlining", len(impulse.Spec.With.Raw)) + } + if template.Spec.ConfigSchema != nil && len(template.Spec.ConfigSchema.Raw) > 0 { + schemaLoader := gojsonschema.NewStringLoader(string(template.Spec.ConfigSchema.Raw)) + documentLoader := gojsonschema.NewStringLoader(string(impulse.Spec.With.Raw)) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error validating spec.with against ImpulseTemplate schema: %w", err) } - // Validate 'with' against the template's configSchema - if template.Spec.ConfigSchema != nil && len(template.Spec.ConfigSchema.Raw) > 0 { - schemaLoader := gojsonschema.NewStringLoader(string(template.Spec.ConfigSchema.Raw)) - documentLoader := gojsonschema.NewStringLoader(string(impulse.Spec.With.Raw)) - result, err := gojsonschema.Validate(schemaLoader, documentLoader) - if err != nil { - return fmt.Errorf("error validating spec.with against ImpulseTemplate schema: %w", err) - } - if !result.Valid() { - var errs []string - for _, desc := range result.Errors() { - errs = append(errs, desc.String()) - } - return fmt.Errorf("spec.with is invalid against ImpulseTemplate schema: %v", errs) + if !result.Valid() { + var errs []string + for _, desc := range result.Errors() { + errs = append(errs, desc.String()) } + return fmt.Errorf("spec.with is invalid against ImpulseTemplate schema: %v", errs) } } - if impulse.Spec.Mapping != nil && len(impulse.Spec.Mapping.Raw) > 0 { - b := impulse.Spec.Mapping.Raw - for len(b) > 0 && (b[0] == ' ' || b[0] == '\n' || b[0] == '\t' || b[0] == '\r') { - b = b[1:] - } - if len(b) > 0 && b[0] != '{' { - return fmt.Errorf("spec.mapping must be a JSON object") - } - // Inline size cap for mapping as well - maxBytes := v.Config.Engram.EngramControllerConfig.DefaultMaxInlineSize - if maxBytes == 0 { - maxBytes = 1024 - } - if len(impulse.Spec.Mapping.Raw) > maxBytes { - return fmt.Errorf("spec.mapping too large (%d bytes). Provide large payloads via object storage and references instead of inlining", len(impulse.Spec.Mapping.Raw)) + return nil +} + +func (v *ImpulseCustomValidator) validateMappingBlock(impulse *bubushv1alpha1.Impulse, template *v1alpha1.ImpulseTemplate) error { + if impulse.Spec.Mapping == nil || len(impulse.Spec.Mapping.Raw) == 0 { + return nil + } + b := impulse.Spec.Mapping.Raw + for len(b) > 0 && (b[0] == ' ' || b[0] == '\n' || b[0] == '\t' || b[0] == '\r') { + b = b[1:] + } + if len(b) > 0 && b[0] != '{' { + return fmt.Errorf("spec.mapping must be a JSON object") + } + maxBytes := v.Config.Engram.EngramControllerConfig.DefaultMaxInlineSize + if maxBytes == 0 { + maxBytes = 1024 + } + if len(impulse.Spec.Mapping.Raw) > maxBytes { + return fmt.Errorf("spec.mapping too large (%d bytes). Provide large payloads via object storage and references instead of inlining", len(impulse.Spec.Mapping.Raw)) + } + if template.Spec.ContextSchema != nil && len(template.Spec.ContextSchema.Raw) > 0 { + schemaLoader := gojsonschema.NewStringLoader(string(template.Spec.ContextSchema.Raw)) + documentLoader := gojsonschema.NewStringLoader(string(impulse.Spec.Mapping.Raw)) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("error validating spec.mapping against ImpulseTemplate schema: %w", err) } - // Validate 'mapping' against the template's contextSchema - if template.Spec.ContextSchema != nil && len(template.Spec.ContextSchema.Raw) > 0 { - schemaLoader := gojsonschema.NewStringLoader(string(template.Spec.ContextSchema.Raw)) - documentLoader := gojsonschema.NewStringLoader(string(impulse.Spec.Mapping.Raw)) - result, err := gojsonschema.Validate(schemaLoader, documentLoader) - if err != nil { - return fmt.Errorf("error validating spec.mapping against ImpulseTemplate schema: %w", err) - } - if !result.Valid() { - var errs []string - for _, desc := range result.Errors() { - errs = append(errs, desc.String()) - } - return fmt.Errorf("spec.mapping is invalid against ImpulseTemplate schema: %v", errs) + if !result.Valid() { + var errs []string + for _, desc := range result.Errors() { + errs = append(errs, desc.String()) } + return fmt.Errorf("spec.mapping is invalid against ImpulseTemplate schema: %v", errs) } } + return nil +} + +func (v *ImpulseCustomValidator) validateWorkloadMode(impulse *bubushv1alpha1.Impulse) error { if impulse.Spec.Workload != nil && impulse.Spec.Workload.Mode == "job" { return fmt.Errorf("spec.workload.mode must not be 'job' for Impulse (must be always-on)") } diff --git a/pkg/cel/cache.go b/pkg/cel/cache.go index 8ac1b0e..71dbca4 100644 --- a/pkg/cel/cache.go +++ b/pkg/cel/cache.go @@ -88,7 +88,10 @@ func NewCompilationCache(config *CacheConfig, env *cel.Env, logger observability } // CompileAndCache compiles a CEL expression and caches the result -func (cc *CompilationCache) CompileAndCache(ctx context.Context, expression, expressionType string) (*CachedProgram, error) { +func (cc *CompilationCache) CompileAndCache( + ctx context.Context, + expression, expressionType string, +) (*CachedProgram, error) { // Generate cache key key := cc.generateCacheKey(expression, expressionType) @@ -170,7 +173,11 @@ func (cc *CompilationCache) CompileAndCache(ctx context.Context, expression, exp } // Evaluate compiles (with caching) and evaluates a CEL expression -func (cc *CompilationCache) Evaluate(ctx context.Context, expression, expressionType string, vars map[string]interface{}) (interface{}, error) { +func (cc *CompilationCache) Evaluate( + ctx context.Context, + expression, expressionType string, + vars map[string]any, +) (any, error) { // Get or compile the program cached, err := cc.CompileAndCache(ctx, expression, expressionType) if err != nil { @@ -201,7 +208,11 @@ func (cc *CompilationCache) Evaluate(ctx context.Context, expression, expression } // EvaluateCondition evaluates a boolean CEL expression with caching -func (cc *CompilationCache) EvaluateCondition(ctx context.Context, expression string, vars map[string]interface{}) (bool, error) { +func (cc *CompilationCache) EvaluateCondition( + ctx context.Context, + expression string, + vars map[string]any, +) (bool, error) { result, err := cc.Evaluate(ctx, expression, "condition", vars) if err != nil { return false, err @@ -215,12 +226,20 @@ func (cc *CompilationCache) EvaluateCondition(ctx context.Context, expression st } // EvaluateTransform evaluates a transform CEL expression with caching -func (cc *CompilationCache) EvaluateTransform(ctx context.Context, expression string, vars map[string]interface{}) (interface{}, error) { +func (cc *CompilationCache) EvaluateTransform( + ctx context.Context, + expression string, + vars map[string]any, +) (any, error) { return cc.Evaluate(ctx, expression, "transform", vars) } // EvaluateFilter evaluates a filter CEL expression with caching -func (cc *CompilationCache) EvaluateFilter(ctx context.Context, expression string, vars map[string]interface{}) (bool, error) { +func (cc *CompilationCache) EvaluateFilter( + ctx context.Context, + expression string, + vars map[string]any, +) (bool, error) { result, err := cc.Evaluate(ctx, expression, "filter", vars) if err != nil { return false, err @@ -333,8 +352,13 @@ func (cc *CompilationCache) cleanup() { } // ProcessList evaluates a CEL expression over a list of items with caching -func (cc *CompilationCache) ProcessList(ctx context.Context, expression, expressionType string, items []interface{}, baseVars map[string]interface{}) ([]interface{}, error) { - var results []interface{} +func (cc *CompilationCache) ProcessList( + ctx context.Context, + expression, expressionType string, + items []any, + baseVars map[string]any, +) ([]any, error) { + results := make([]any, 0, len(items)) // Compile the expression once and reuse for all items cached, err := cc.CompileAndCache(ctx, expression, expressionType) @@ -344,7 +368,7 @@ func (cc *CompilationCache) ProcessList(ctx context.Context, expression, express for i, item := range items { // Create evaluation context for this item - vars := make(map[string]interface{}) + vars := make(map[string]any) for k, v := range baseVars { vars[k] = v } diff --git a/pkg/cel/evaluator.go b/pkg/cel/evaluator.go index d2e3ad3..b1d1ea5 100644 --- a/pkg/cel/evaluator.go +++ b/pkg/cel/evaluator.go @@ -59,7 +59,7 @@ func (e *Evaluator) Close() { } // EvaluateWhenCondition evaluates a step's `when` condition. -func (e *Evaluator) EvaluateWhenCondition(ctx context.Context, when string, vars map[string]interface{}) (bool, error) { +func (e *Evaluator) EvaluateWhenCondition(ctx context.Context, when string, vars map[string]any) (bool, error) { if strings.TrimSpace(when) == "" { return true, nil } @@ -83,12 +83,16 @@ func (e *Evaluator) EvaluateWhenCondition(ctx context.Context, when string, vars } // ResolveWithInputs resolves the `with` block for a step. -func (e *Evaluator) ResolveWithInputs(ctx context.Context, with map[string]interface{}, vars map[string]interface{}) (map[string]interface{}, error) { +func (e *Evaluator) ResolveWithInputs( + ctx context.Context, + with map[string]any, + vars map[string]any, +) (map[string]any, error) { if with == nil { return nil, nil } - resolved := make(map[string]interface{}) + resolved := make(map[string]any) for key, val := range with { strVal, ok := val.(string) if !ok { diff --git a/pkg/cel/expressions.go b/pkg/cel/expressions.go index f4431a6..b0d84da 100644 --- a/pkg/cel/expressions.go +++ b/pkg/cel/expressions.go @@ -39,17 +39,17 @@ type ExpressionResolver struct { } // NewExpressionResolver creates a new expression resolver -func NewExpressionResolver(client client.Client, namespace, storyRun string) *ExpressionResolver { +func NewExpressionResolver(k8sClient client.Client, namespace, storyRun string) *ExpressionResolver { return &ExpressionResolver{ - client: client, + client: k8sClient, namespace: namespace, storyRun: storyRun, } } // ResolveInputs resolves GitHub Actions-style expressions in step inputs -func (r *ExpressionResolver) ResolveInputs(ctx context.Context, inputs map[string]interface{}) (map[string]interface{}, error) { - resolved := make(map[string]interface{}) +func (r *ExpressionResolver) ResolveInputs(ctx context.Context, inputs map[string]any) (map[string]any, error) { + resolved := make(map[string]any) for key, value := range inputs { resolvedValue, err := r.resolveValue(ctx, value) @@ -63,13 +63,13 @@ func (r *ExpressionResolver) ResolveInputs(ctx context.Context, inputs map[strin } // resolveValue recursively resolves expressions in any value type -func (r *ExpressionResolver) resolveValue(ctx context.Context, value interface{}) (interface{}, error) { +func (r *ExpressionResolver) resolveValue(ctx context.Context, value any) (any, error) { switch v := value.(type) { case string: return r.resolveString(ctx, v) - case map[string]interface{}: + case map[string]any: return r.resolveMap(ctx, v) - case []interface{}: + case []any: return r.resolveArray(ctx, v) default: return value, nil @@ -77,7 +77,7 @@ func (r *ExpressionResolver) resolveValue(ctx context.Context, value interface{} } // resolveString resolves expressions in string values -func (r *ExpressionResolver) resolveString(ctx context.Context, str string) (interface{}, error) { +func (r *ExpressionResolver) resolveString(ctx context.Context, str string) (any, error) { // Handle ${{ steps.step-name.outputs.field }} expressions if strings.Contains(str, "${{ steps.") { return r.resolveStepsExpression(ctx, str) @@ -102,8 +102,8 @@ func (r *ExpressionResolver) resolveString(ctx context.Context, str string) (int } // resolveMap resolves expressions in map values -func (r *ExpressionResolver) resolveMap(ctx context.Context, m map[string]interface{}) (map[string]interface{}, error) { - resolved := make(map[string]interface{}) +func (r *ExpressionResolver) resolveMap(ctx context.Context, m map[string]any) (map[string]any, error) { + resolved := make(map[string]any) for key, value := range m { resolvedValue, err := r.resolveValue(ctx, value) @@ -117,8 +117,8 @@ func (r *ExpressionResolver) resolveMap(ctx context.Context, m map[string]interf } // resolveArray resolves expressions in array values -func (r *ExpressionResolver) resolveArray(ctx context.Context, arr []interface{}) ([]interface{}, error) { - resolved := make([]interface{}, len(arr)) +func (r *ExpressionResolver) resolveArray(ctx context.Context, arr []any) ([]any, error) { + resolved := make([]any, len(arr)) for i, value := range arr { resolvedValue, err := r.resolveValue(ctx, value) @@ -132,7 +132,7 @@ func (r *ExpressionResolver) resolveArray(ctx context.Context, arr []interface{} } // resolveStepsExpression resolves ${{ steps.step-name.outputs.field }} expressions -func (r *ExpressionResolver) resolveStepsExpression(ctx context.Context, expression string) (interface{}, error) { +func (r *ExpressionResolver) resolveStepsExpression(ctx context.Context, expression string) (any, error) { // Parse expression: ${{ steps.extract-data.outputs.data }} re := regexp.MustCompile(`\$\{\{\s*steps\.([^.]+)\.outputs\.([^}\s]+)\s*\}\}`) matches := re.FindStringSubmatch(expression) @@ -163,7 +163,7 @@ func (r *ExpressionResolver) resolveStepsExpression(ctx context.Context, express } // Parse output JSON - var output map[string]interface{} + var output map[string]any if err := json.Unmarshal(stepRun.Status.Output.Raw, &output); err != nil { return nil, fmt.Errorf("failed to parse output from step %s: %w", stepID, err) } @@ -181,7 +181,7 @@ func (r *ExpressionResolver) resolveStepsExpression(ctx context.Context, express } // resolveInputsExpression resolves ${{ inputs.field }} expressions -func (r *ExpressionResolver) resolveInputsExpression(ctx context.Context, expression string) (interface{}, error) { +func (r *ExpressionResolver) resolveInputsExpression(ctx context.Context, expression string) (any, error) { // Parse expression: ${{ inputs.user_data }} re := regexp.MustCompile(`\$\{\{\s*inputs\.([^}\s]+)\s*\}\}`) matches := re.FindStringSubmatch(expression) @@ -208,7 +208,7 @@ func (r *ExpressionResolver) resolveInputsExpression(ctx context.Context, expres } // Parse inputs JSON - var inputs map[string]interface{} + var inputs map[string]any if err := json.Unmarshal(storyRun.Spec.Inputs.Raw, &inputs); err != nil { return nil, fmt.Errorf("failed to parse StoryRun inputs: %w", err) } @@ -222,7 +222,7 @@ func (r *ExpressionResolver) resolveInputsExpression(ctx context.Context, expres } // resolveSecretsExpression resolves ${{ secrets.secret-name }} or ${{ secrets.secret-name.key }} expressions -func (r *ExpressionResolver) resolveSecretsExpression(ctx context.Context, expression string) (interface{}, error) { +func (r *ExpressionResolver) resolveSecretsExpression(ctx context.Context, expression string) (any, error) { // Parse expression: ${{ secrets.api-key }} or ${{ secrets.database-creds.PASSWORD }} re := regexp.MustCompile(`\$\{\{\s*secrets\.([^.}\s]+)(?:\.([^}\s]+))?\s*\}\}`) matches := re.FindStringSubmatch(expression) diff --git a/pkg/cel/helpers.go b/pkg/cel/helpers.go index 34629e4..2f96aa1 100644 --- a/pkg/cel/helpers.go +++ b/pkg/cel/helpers.go @@ -135,7 +135,7 @@ func (d *dataOperationsLib) ProgramOptions() []cel.ProgramOption { // CEL function implementations func sortByField(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("sort_by requires a list") } @@ -146,7 +146,7 @@ func sortByField(lhs, rhs ref.Val) ref.Val { } // Create a copy to sort - sorted := make([]interface{}, len(list)) + sorted := make([]any, len(list)) copy(sorted, list) sort.Slice(sorted, func(i, j int) bool { @@ -159,13 +159,13 @@ func sortByField(lhs, rhs ref.Val) ref.Val { } func deduplicate(arg ref.Val) ref.Val { - list, ok := arg.Value().([]interface{}) + list, ok := arg.Value().([]any) if !ok { return types.NewErr("dedupe requires a list") } seen := make(map[string]bool) - var result []interface{} + var result []any for _, item := range list { key := fmt.Sprintf("%v", item) @@ -179,7 +179,7 @@ func deduplicate(arg ref.Val) ref.Val { } func groupByField(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("group_by requires a list") } @@ -189,7 +189,7 @@ func groupByField(lhs, rhs ref.Val) ref.Val { return types.NewErr("group_by requires a string field name") } - groups := make(map[string][]interface{}) + groups := make(map[string][]any) for _, item := range list { key := fmt.Sprintf("%v", getFieldValue(item, field)) @@ -200,7 +200,7 @@ func groupByField(lhs, rhs ref.Val) ref.Val { } func chunkList(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("chunk requires a list") } @@ -214,7 +214,7 @@ func chunkList(lhs, rhs ref.Val) ref.Val { return types.NewErr("chunk size must be positive") } - var chunks [][]interface{} + var chunks [][]any for i := 0; i < len(list); i += int(size) { end := i + int(size) if end > len(list) { @@ -227,7 +227,7 @@ func chunkList(lhs, rhs ref.Val) ref.Val { } func sumByField(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("sum_by requires a list") } @@ -249,7 +249,7 @@ func sumByField(lhs, rhs ref.Val) ref.Val { } func countByField(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("count_by requires a list") } @@ -270,17 +270,17 @@ func countByField(lhs, rhs ref.Val) ref.Val { } func renameKeys(lhs, rhs ref.Val) ref.Val { - data, ok := lhs.Value().(map[string]interface{}) + data, ok := lhs.Value().(map[string]any) if !ok { return types.NewErr("rename_keys requires a map") } - mapping, ok := rhs.Value().(map[string]interface{}) + mapping, ok := rhs.Value().(map[string]any) if !ok { return types.NewErr("rename_keys requires a mapping") } - result := make(map[string]interface{}) + result := make(map[string]any) for key, value := range data { if newKey, exists := mapping[key]; exists { @@ -314,7 +314,7 @@ func formatTime(lhs, rhs ref.Val) ref.Val { } func takeN(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("take requires a list") } @@ -325,7 +325,7 @@ func takeN(lhs, rhs ref.Val) ref.Val { } if n <= 0 { - return types.DefaultTypeAdapter.NativeToValue([]interface{}{}) + return types.DefaultTypeAdapter.NativeToValue([]any{}) } if int(n) >= len(list) { @@ -336,7 +336,7 @@ func takeN(lhs, rhs ref.Val) ref.Val { } func skipN(lhs, rhs ref.Val) ref.Val { - list, ok := lhs.Value().([]interface{}) + list, ok := lhs.Value().([]any) if !ok { return types.NewErr("skip requires a list") } @@ -351,7 +351,7 @@ func skipN(lhs, rhs ref.Val) ref.Val { } if int(n) >= len(list) { - return types.DefaultTypeAdapter.NativeToValue([]interface{}{}) + return types.DefaultTypeAdapter.NativeToValue([]any{}) } return types.DefaultTypeAdapter.NativeToValue(list[n:]) @@ -359,14 +359,14 @@ func skipN(lhs, rhs ref.Val) ref.Val { // Helper functions -func getFieldValue(item interface{}, field string) interface{} { - if m, ok := item.(map[string]interface{}); ok { +func getFieldValue(item any, field string) any { + if m, ok := item.(map[string]any); ok { return m[field] } return nil } -func compareValues(a, b interface{}) int { +func compareValues(a, b any) int { switch va := a.(type) { case string: if vb, ok := b.(string); ok { @@ -398,7 +398,7 @@ func compareValues(a, b interface{}) int { return 0 } -func toNumber(val interface{}) (float64, bool) { +func toNumber(val any) (float64, bool) { switch v := val.(type) { case int64: return float64(v), true diff --git a/pkg/conditions/conditions.go b/pkg/conditions/conditions.go index 6d586d3..389b050 100644 --- a/pkg/conditions/conditions.go +++ b/pkg/conditions/conditions.go @@ -112,7 +112,13 @@ func (cm *ConditionManager) UpdateGeneration(generation int64) { } // SetCondition sets a condition on the conditions slice -func (cm *ConditionManager) SetCondition(conditions *[]metav1.Condition, conditionType string, status metav1.ConditionStatus, reason, message string) { +func (cm *ConditionManager) SetCondition( + conditions *[]metav1.Condition, + conditionType string, + status metav1.ConditionStatus, + reason, + message string, +) { now := metav1.Now() condition := metav1.Condition{ Type: conditionType, @@ -136,7 +142,12 @@ func (cm *ConditionManager) SetReadyCondition(conditions *[]metav1.Condition, re } // SetProgressingCondition sets the Progressing condition -func (cm *ConditionManager) SetProgressingCondition(conditions *[]metav1.Condition, progressing bool, reason, message string) { +func (cm *ConditionManager) SetProgressingCondition( + conditions *[]metav1.Condition, + progressing bool, + reason, + message string, +) { status := metav1.ConditionFalse if progressing { status = metav1.ConditionTrue @@ -145,7 +156,12 @@ func (cm *ConditionManager) SetProgressingCondition(conditions *[]metav1.Conditi } // SetDegradedCondition sets the Degraded condition -func (cm *ConditionManager) SetDegradedCondition(conditions *[]metav1.Condition, degraded bool, reason, message string) { +func (cm *ConditionManager) SetDegradedCondition( + conditions *[]metav1.Condition, + degraded bool, + reason, + message string, +) { status := metav1.ConditionFalse if degraded { status = metav1.ConditionTrue @@ -154,7 +170,12 @@ func (cm *ConditionManager) SetDegradedCondition(conditions *[]metav1.Condition, } // SetTerminatingCondition sets the Terminating condition -func (cm *ConditionManager) SetTerminatingCondition(conditions *[]metav1.Condition, terminating bool, reason, message string) { +func (cm *ConditionManager) SetTerminatingCondition( + conditions *[]metav1.Condition, + terminating bool, + reason, + message string, +) { status := metav1.ConditionFalse if terminating { status = metav1.ConditionTrue @@ -224,7 +245,12 @@ func (cm *ConditionManager) setConditionInternal(conditions *[]metav1.Condition, } // TransitionConditions handles standard resource state transitions -func (cm *ConditionManager) TransitionConditions(conditions *[]metav1.Condition, targetState string, reason, message string) { +func (cm *ConditionManager) TransitionConditions( + conditions *[]metav1.Condition, + targetState string, + reason, + message string, +) { switch targetState { case "Ready": cm.SetReadyCondition(conditions, true, reason, message) @@ -253,7 +279,9 @@ func (cm *ConditionManager) TransitionConditions(conditions *[]metav1.Condition, } // SetLargeDataDelegatedCondition sets the LargeDataDelegated condition -func (cm *ConditionManager) SetLargeDataDelegatedCondition(conditions *[]metav1.Condition, delegated bool, reason, message string) { +func (cm *ConditionManager) SetLargeDataDelegatedCondition( + conditions *[]metav1.Condition, delegated bool, reason, message string, +) { status := metav1.ConditionFalse if delegated { status = metav1.ConditionTrue diff --git a/pkg/enums/enums.go b/pkg/enums/enums.go index e65effa..8c0500e 100644 --- a/pkg/enums/enums.go +++ b/pkg/enums/enums.go @@ -29,7 +29,7 @@ package enums // Pending → Running → {Succeeded|Failed|Canceled|Compensated|Paused|Blocked|Scheduling|Timeout|Aborted} // // Some resources may also support Paused for manual intervention scenarios. -// +// nolint:lll // +kubebuilder:validation:Enum=Pending;Running;Succeeded;Failed;Canceled;Compensated;Paused;Blocked;Scheduling;Timeout;Aborted type Phase string @@ -125,7 +125,7 @@ const ( // StepType defines the type of step in a story workflow. // BubuStack provides built-in primitives for common workflow patterns, // reducing the need for custom code in many scenarios. -// +// nolint:lll // +kubebuilder:validation:Enum=condition;loop;parallel;sleep;stop;switch;filter;transform;wait;throttle;batch;executeStory;setData;mergeData;gate type StepType string diff --git a/pkg/logging/structured.go b/pkg/logging/structured.go index 3ad90de..aac2440 100644 --- a/pkg/logging/structured.go +++ b/pkg/logging/structured.go @@ -110,18 +110,18 @@ func (cl *ControllerLogger) WithError(err error) *ControllerLogger { } // WithValues adds custom key-value pairs to the logger -func (cl *ControllerLogger) WithValues(keysAndValues ...interface{}) *ControllerLogger { +func (cl *ControllerLogger) WithValues(keysAndValues ...any) *ControllerLogger { logger := cl.logger.WithValues(keysAndValues...) return &ControllerLogger{logger: logger} } // Info logs an info message -func (cl *ControllerLogger) Info(msg string, keysAndValues ...interface{}) { +func (cl *ControllerLogger) Info(msg string, keysAndValues ...any) { cl.logger.Info(msg, keysAndValues...) } // Error logs an error message -func (cl *ControllerLogger) Error(err error, msg string, keysAndValues ...interface{}) { +func (cl *ControllerLogger) Error(err error, msg string, keysAndValues ...any) { cl.logger.Error(err, msg, keysAndValues...) } @@ -145,26 +145,26 @@ func NewReconcileLogger(ctx context.Context, controller string) *ReconcileLogger } // ReconcileStart logs the start of reconciliation -func (rl *ReconcileLogger) ReconcileStart(msg string, keysAndValues ...interface{}) { +func (rl *ReconcileLogger) ReconcileStart(msg string, keysAndValues ...any) { rl.Info("Reconcile started: "+msg, keysAndValues...) } // ReconcileSuccess logs successful reconciliation -func (rl *ReconcileLogger) ReconcileSuccess(msg string, keysAndValues ...interface{}) { +func (rl *ReconcileLogger) ReconcileSuccess(msg string, keysAndValues ...any) { duration := time.Since(rl.startTime) allValues := append(keysAndValues, "duration", duration.String()) rl.Info("Reconcile succeeded: "+msg, allValues...) } // ReconcileError logs reconciliation error -func (rl *ReconcileLogger) ReconcileError(err error, msg string, keysAndValues ...interface{}) { +func (rl *ReconcileLogger) ReconcileError(err error, msg string, keysAndValues ...any) { duration := time.Since(rl.startTime) allValues := append(keysAndValues, "duration", duration.String()) rl.Error(err, "Reconcile failed: "+msg, allValues...) } // ReconcileRequeue logs reconciliation requeue -func (rl *ReconcileLogger) ReconcileRequeue(msg string, after time.Duration, keysAndValues ...interface{}) { +func (rl *ReconcileLogger) ReconcileRequeue(msg string, after time.Duration, keysAndValues ...any) { duration := time.Since(rl.startTime) allValues := append(keysAndValues, "duration", duration.String(), "requeue_after", after.String()) rl.Info("Reconcile requeue: "+msg, allValues...) @@ -182,28 +182,28 @@ func NewStepLogger(ctx context.Context, steprun *runsv1alpha1.StepRun) *StepLogg } // StepStart logs step execution start -func (sl *StepLogger) StepStart(msg string, keysAndValues ...interface{}) { +func (sl *StepLogger) StepStart(msg string, keysAndValues ...any) { sl.Info("Step started: "+msg, keysAndValues...) } // StepProgress logs step execution progress -func (sl *StepLogger) StepProgress(msg string, keysAndValues ...interface{}) { +func (sl *StepLogger) StepProgress(msg string, keysAndValues ...any) { sl.V(1).Info("Step progress: "+msg, keysAndValues...) } // StepSuccess logs step execution success -func (sl *StepLogger) StepSuccess(msg string, duration time.Duration, keysAndValues ...interface{}) { +func (sl *StepLogger) StepSuccess(msg string, duration time.Duration, keysAndValues ...any) { allValues := append(keysAndValues, "duration", duration.String()) sl.Info("Step succeeded: "+msg, allValues...) } // StepError logs step execution error -func (sl *StepLogger) StepError(err error, msg string, keysAndValues ...interface{}) { +func (sl *StepLogger) StepError(err error, msg string, keysAndValues ...any) { sl.Error(err, "Step failed: "+msg, keysAndValues...) } // StepRetry logs step retry -func (sl *StepLogger) StepRetry(attempt int, reason string, keysAndValues ...interface{}) { +func (sl *StepLogger) StepRetry(attempt int, reason string, keysAndValues ...any) { allValues := append(keysAndValues, "attempt", attempt, "reason", reason) sl.Info("Step retry", allValues...) } @@ -232,12 +232,27 @@ func (l *CELLogger) EvaluationStart(expression, expressionType string) { // EvaluationError logs a CEL evaluation error func (l *CELLogger) EvaluationError(err error, expression, expressionType string, duration time.Duration) { - l.log.Error(err, "CEL evaluation failed", "expression", expression, "type", expressionType, "duration", duration.String()) + l.log.Error( + err, + "CEL evaluation failed", + "expression", + expression, + "type", + expressionType, + "duration", + duration.String(), + ) } // EvaluationSuccess logs a successful CEL evaluation -func (l *CELLogger) EvaluationSuccess(expression, expressionType string, duration time.Duration, result interface{}) { - l.log.V(1).Info("CEL evaluation succeeded", "expression", expression, "type", expressionType, "duration", duration.String(), "result", result) +func (l *CELLogger) EvaluationSuccess(expression, expressionType string, duration time.Duration, result any) { + l.log.V(1).Info( + "CEL evaluation succeeded", + "expression", expression, + "type", expressionType, + "duration", duration.String(), + "result", result, + ) } // CleanupLogger provides specialized logging for cleanup operations @@ -252,19 +267,19 @@ func NewCleanupLogger(ctx context.Context, resourceType string) *CleanupLogger { } // CleanupStart logs cleanup start -func (cl *CleanupLogger) CleanupStart(resourceName string, keysAndValues ...interface{}) { +func (cl *CleanupLogger) CleanupStart(resourceName string, keysAndValues ...any) { allValues := append(keysAndValues, "resource", resourceName) cl.Info("Cleanup started", allValues...) } // CleanupSuccess logs cleanup success -func (cl *CleanupLogger) CleanupSuccess(resourceName string, duration time.Duration, keysAndValues ...interface{}) { +func (cl *CleanupLogger) CleanupSuccess(resourceName string, duration time.Duration, keysAndValues ...any) { allValues := append(keysAndValues, "resource", resourceName, "duration", duration.String()) cl.Info("Cleanup succeeded", allValues...) } // CleanupError logs cleanup error -func (cl *CleanupLogger) CleanupError(err error, resourceName string, keysAndValues ...interface{}) { +func (cl *CleanupLogger) CleanupError(err error, resourceName string, keysAndValues ...any) { allValues := append(keysAndValues, "resource", resourceName) cl.Error(err, "Cleanup failed", allValues...) } diff --git a/pkg/metrics/controller_metrics.go b/pkg/metrics/controller_metrics.go index 4c8d662..76e5d12 100644 --- a/pkg/metrics/controller_metrics.go +++ b/pkg/metrics/controller_metrics.go @@ -24,6 +24,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics" ) +const ( + resultSuccess = "success" + resultError = "error" +) + var ( // StoryRun metrics StoryRunsTotal = prometheus.NewCounterVec( @@ -299,9 +304,9 @@ func RecordStepRunDuration(namespace, storyRunRef, stepName, phase string, durat // RecordControllerReconcile records controller reconcile metrics func RecordControllerReconcile(controller string, duration time.Duration, err error) { - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError ControllerReconcileErrors.WithLabelValues(controller, classifyError(err)).Inc() } @@ -311,9 +316,9 @@ func RecordControllerReconcile(controller string, duration time.Duration, err er // RecordCELEvaluation records CEL evaluation metrics func RecordCELEvaluation(expressionType string, duration time.Duration, err error) { - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError } CELEvaluationTotal.WithLabelValues(expressionType, result).Inc() @@ -327,9 +332,9 @@ func RecordCELCacheHit(cacheType string) { // RecordResourceCleanup records resource cleanup metrics func RecordResourceCleanup(resourceType string, duration time.Duration, err error) { - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError } ResourceCleanupTotal.WithLabelValues(resourceType, result).Inc() @@ -409,9 +414,9 @@ func UpdateResourceQuotaLimit(namespace, resourceType string, limit float64) { func RecordStorageOperation(provider, operation string, duration time.Duration, err error) { // This would be implemented properly in the storage package // For now, just record as a generic operation - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError } // Use existing controller metrics as placeholder @@ -421,9 +426,9 @@ func RecordStorageOperation(provider, operation string, duration time.Duration, // RecordArtifactOperation records artifact management operations func RecordArtifactOperation(operation string, duration time.Duration, err error) { - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError } // Use existing controller metrics for artifact operations ControllerReconcileTotal.WithLabelValues("artifact-manager", result).Inc() @@ -432,9 +437,9 @@ func RecordArtifactOperation(operation string, duration time.Duration, err error // RecordCleanupOperation records cleanup operations func RecordCleanupOperation(resourceType, namespace string, deletedCount int, duration time.Duration, err error) { - result := "success" + result := resultSuccess if err != nil { - result = "error" + result = resultError } // Use existing controller metrics for cleanup operations diff --git a/pkg/observability/interfaces.go b/pkg/observability/interfaces.go index 1a7c897..4265bae 100644 --- a/pkg/observability/interfaces.go +++ b/pkg/observability/interfaces.go @@ -7,5 +7,5 @@ type Logger interface { CacheHit(expression, expressionType string) EvaluationStart(expression, expressionType string) EvaluationError(err error, expression, expressionType string, duration time.Duration) - EvaluationSuccess(expression, expressionType string, duration time.Duration, result interface{}) + EvaluationSuccess(expression, expressionType string, duration time.Duration, result any) }