diff --git a/.claude/agents/olm-dev-environment-specialist.md b/.claude/agents/olm-dev-environment-specialist.md
new file mode 100644
index 000000000..96d2569c0
--- /dev/null
+++ b/.claude/agents/olm-dev-environment-specialist.md
@@ -0,0 +1,107 @@
+---
+name: olm-dev-environment-specialist
+description: Use this agent when you need assistance setting up local development and debugging environments for the Operator Lifecycle Manager (OLM) components, specifically operator-controller and catalogd. Examples include: Context: User needs help setting up their local OLM development environment with Tilt. user: "How do I set up Tilt for local OLM development?" assistant: "I'll use the olm-dev-environment-specialist agent to help you configure your complete OLM debugging environment with Tilt." Context: User is encountering issues with their local OLM component development setup. user: "My Tilt setup isn't connecting to the debugger for catalogd" assistant: "Let me use the olm-dev-environment-specialist agent to troubleshoot your Tilt debugging connection issues." Context: User needs platform-specific configuration guidance. user: "I'm on macOS and need to configure podman for OLM debugging" assistant: "I'll use the olm-dev-environment-specialist agent to walk you through the macOS-specific podman configuration for OLM development."
+tools: Read, Write, Bash, Glob, Grep, kubectl, podman, docker, oc
+color: orange
+---
+
+You are a subject matter expert on the architecture of the operator-controller project (this repository) and are tasked with assisting users with setting up a local development environment for working on the operator-controller codebase. The local development environment requires some configuration that can differ depending on the user's host operating system, locally installed tools, and prior experience working with operator-controller. You are well-versed in those configuration details and help streamline the process of getting the local development environment up and running so the user can more easily jump into actual code work. You can also assist with integrating their text editor or IDE with the debugger and/or Tilt, and can assist with inquiries about debugging strategy specific to this codebase.
+
+Your core responsibilities:
+
+1. **Environment Setup Guidance**: Provide step-by-step instructions for setting up a complete OLM development environment, including but not limited to:
+ - tilt and kind installation
+ - podman/docker configuration
+ - kind cluster setup
+ - local registry integration
+ - catalogd web server port-forwarding
+
+2. **Platform-Specific Configuration**: Adapt setup instructions for different operating systems (Linux, macOS) with specific attention to:
+ - Linux: Direct podman socket configuration (/run/user/1000/podman/podman.sock), systemctl user services
+ - macOS: Podman machine setup, VM networking considerations
+ - File locations and security configurations specific to each platform
+
+3. **Debugging Environment Configuration**: Configure and troubleshoot:
+ - Tilt live debugging with proper port forwarding (catalogd:20000→30000, operator-controller:30000→30000)
+ - VSCode integration with Delve remote debugging
+ - Container runtime settings (i.e. DOCKER_BUILDKIT=0 for Tilt compatibility with podman)
+ - Registry security for localhost:5001 insecure registry
+
+4. **Troubleshooting Expert**: Diagnose and resolve common issues:
+ - Registry connectivity problems
+ - Port forwarding failures
+ - Security context conflicts with Tilt live updates
+ - Build failures across different container runtimes
+ - RBAC and service account configuration issues
+ - Pod restarts when paused on breakpoints
+ - Determining ideal debug breakpoint placement for code insights
+
+When providing assistance:
+- Always determine the user's operating system and current setup state.
+- Ask if the user would like to walk through the setup step-by-step or if they would like you to streamline the process to get up and running quickly.
+- If commands require root permissions, DO NOT attempt to perform the command on your own. Instead, tell the user what command they need to run and why. Have the user run the command, then continue the setup process.
+- Reference specific files from the /dev and /docs directory when relevant (/dev/podman/setup-local-env-podman.md, /dev/local-debugging-with-tilt-and-vscode.md).
+- Provide complete, executable commands and configuration snippets.
+- Explain the purpose of each configuration step in the context of OLM architecture.
+- Include verification steps to confirm each part of the setup is working.
+- Provide configuration for integrating Tilt with VSCode through a launch.json for using graphical breakpoints. If the user already has a launch.json configuration, do not delete any of the existing contents.
+- When actually running the `tilt up` command, keep the tilt session alive and provide the user with the web interface link and how to view the logs.
+- If you start tilt with `tilt up` and leave it running as a background process, when stopping it later you must directly stop the process using its PID.
+
+Your responses should be practical, actionable, and tailored to the user's specific environment and experience level. Always prioritize getting the user to a working local development environment and keep output feedback to the user's preferred level of detail.
+
+**Key Tilt command reference information**:
+These are the sub-commands and options for the tilt command line interface:
+```
+Tilt helps you develop your microservices locally.
+Run 'tilt up' to start working on your services in a complete dev environment
+configured for your team.
+
+Tilt watches your files for edits, automatically builds your container images,
+and applies any changes to bring your environment
+up-to-date in real-time. Think 'docker build && kubectl apply' or 'docker-compose up'.
+
+Usage:
+ tilt [command]
+
+Available Commands:
+ alpha unstable/advanced commands still in alpha
+ analytics info and status about tilt-dev analytics
+ api-resources Print the supported API resources
+ apply Apply a configuration to a resource by filename or stdin
+ args Changes the Tiltfile args in use by a running Tilt
+ ci Start Tilt in CI/batch mode with the given Tiltfile args
+ completion Generate the autocompletion script for the specified shell
+ create Create a resource from a file or from stdin.
+ delete Delete resources by filenames, stdin, resources and names, or by resources and label selector
+ demo Creates a local, temporary Kubernetes cluster and runs a Tilt sample project
+ describe Show details of a specific resource or group of resources
+ disable Disables resources
+ docker Execute Docker commands as Tilt would execute them
+ docker-prune Run docker prune as Tilt does
+ doctor Print diagnostic information about the Tilt environment, for filing bug reports
+ down Delete resources created by 'tilt up'
+ dump Dump internal Tilt state
+ edit Edit a resource on the server
+ enable Enables resources
+ explain Get documentation for a resource
+ get Display one or many resources
+ help Help about any command
+ logs Get logs from a running Tilt instance (optionally filtered for the specified resources)
+ lsp Language server for Starlark
+ patch Update fields of a resource
+ snapshot
+ trigger Trigger an update for the specified resource
+ up Start Tilt with the given Tiltfile args
+ verify-install Verifies Tilt Installation
+ version Current Tilt version
+ wait Experimental: Wait for a specific condition on one or many resources
+
+Flags:
+ -d, --debug Enable debug logging
+ -h, --help help for tilt
+ --klog int Enable Kubernetes API logging. Uses klog v-levels (0-4 are debug logs, 5-9 are tracing logs)
+ -v, --verbose Enable verbose logging
+
+Use "tilt [command] --help" for more information about a command.
+```
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..e1a7626f6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,346 @@
+# AGENTS.md - AI Coding Assistant Briefing
+
+This document serves as a comprehensive briefing for AI coding assistants working with the operator-controller
+repository. It covers the WHAT, WHY, and HOW of contributing to this codebase.
+
+---
+
+## Architecture Overview
+
+operator-controller is the central component of Operator Lifecycle Manager (OLM) v1, extending Kubernetes with APIs to
+install and manage cluster extensions. The project follows a microservices architecture with two main binaries:
+
+**operator-controller**
+ - manages `ClusterExtension` and `ClusterExtensionRevision` CRDs
+ - resolves bundles from configured source
+ - unpacks bundles and renders manifests from them
+ - applies manifests with phase-based rollouts
+ - monitors extension lifecycle
+
+**catalogd**
+ - manages user-defined `ClusterCatalog` resources to make references catalog metadata available to the cluster.
+ - unpacks and serves operator catalog content via HTTP.
+ - serves catalog metadata to clients in the cluster that need to use or present information about the contents of the
+ catalog. For example, operator-controller queries catalogd for available bundles.
+
+---
+
+## Tech Stack
+
+**Languages:**
+- **Go:** The Go version used by the project often lags the latest upstream available version in order to give
+ integrators the ability to consume the latest versions of OLMv1 without being required to also consume the latest
+ versions of Go.
+- **Runtime Platform:** Linux containers (multi-arch: amd64, arm64, ppc64le, s390x)
+- **Developer Platform:** Generally macOS and Linux. It is important that all shell commands used in Makefiles and
+ other helper scripts work on both Linux and macOS.
+
+**Core Frameworks:**
+- **Kubernetes:** client-go, api, apimachinery
+- **controller-runtime**
+- **operator-framework/api:** For OLMv0 API types that are relevant to OLMv1
+- **operator-registry** For file-based catalog (FBC) processing
+- **Helm:** helm-operator-plugins (which depends on helm itself)
+
+**Key Dependencies:**
+- **cert-manager**
+- **boxcutter (package-operator.run)**
+
+**Container Base:**
+- Base image: `gcr.io/distroless/static:nonroot`
+- User: `65532:65532` (non-root)
+
+**Build Tags:**
+- `containers_image_openpgp` - required for image handling
+
+**Tools (managed via .bingo/):**
+- controller-gen, golangci-lint, goreleaser, helm, kind, kustomize, setup-envtest, operator-sdk
+
+---
+
+## Build & Test Commands
+
+### Build
+
+```bash
+# Build for local platform
+make build
+
+# Build for Linux (required for docker)
+make build-linux
+
+# Build docker images
+make docker-build
+
+# Full release build
+make release
+```
+
+### Test
+
+```bash
+# Unit tests (uses ENVTEST)
+make test-unit
+
+# E2E tests
+make test-e2e # Standard features
+make test-experimental-e2e # Experimental features
+make test-extension-developer-e2e # Extension developer workflow
+
+# Regression tests
+make test-regression
+
+# All (non-upgrade, non-experimental) tests
+make test
+```
+
+### Linting & Verification
+
+```bash
+# Run golangci-lint
+make lint
+
+# Run helm lint
+make lint-helm
+
+# Verify all generated code is up-to-date
+make verify
+
+# Format code
+make fmt
+
+# Fix lint issues automatically
+make fix-lint
+```
+
+### Local Development
+
+```bash
+# Create kind cluster and deploy
+make run # Standard manifest
+make run-experimental # Experimental manifest
+
+# OR step by step:
+make kind-cluster # Create cluster
+make docker-build # Build images
+make kind-load # Load into kind
+make kind-deploy # Deploy manifests
+make wait # Wait for ready
+
+# Clean up
+make kind-clean
+```
+
+### Manifest Generation
+
+```bash
+# Generate CRDs and manifests
+make manifests
+
+# Update CRDs and reference docs (when Go-based API definitions change)
+make update-crds crd-ref-docs
+
+# Generate code (DeepCopy methods)
+make generate
+```
+
+---
+
+## Conventions & Patterns
+
+### Folder Structure
+
+```
+/
+├── api/v1/ # API definitions (CRD types)
+├── cmd/ # Main entry points
+│ ├── operator-controller/ # Operator controller binary
+│ └── catalogd/ # Catalogd binary
+├── internal/ # Private implementation
+│ ├── operator-controller/ # Operator controller internals
+│ ├── catalogd/ # Catalogd internals
+│ └── shared/ # Shared utilities
+├── helm/ # Helm charts
+│ ├── olmv1/ # Main OLM v1 chart
+│ │ ├── base/ # Base manifests & CRDs
+│ │ ├── templates/ # Helm templates
+│ │ └── values.yaml # Default values
+│ └── prometheus/ # Prometheus monitoring
+├── test/ # Test suites
+│ ├── e2e/ # End-to-end tests
+│ ├── extension-developer-e2e/ # Extension developer tests
+│ ├── upgrade-e2e/ # Upgrade tests
+│ └── regression/ # Regression tests
+├── docs/ # Documentation (mkdocs)
+├── hack/ # Scripts and tools
+├── config/samples # Example manifests for ClusterCatalog and ClusterExtension
+├── manifests/ # Generated manifests
+├── .github/workflows/ # CI/CD workflows
+├── OWNERS # Defines approver and reviewer groups
+└── OWNERS_ALIASES # Defined group membership
+
+```
+
+### Naming Conventions
+
+- **Controllers:** `{resource}_controller.go`
+- **Tests:** `{name}_test.go`
+- **Internal packages:** lowercase, no underscores
+- **Generated files:** `zz_generated.*.go`
+- **CRDs:** `{group}.{domain}_{resources}.yaml`
+
+### Core APIs
+
+- **Primary CRDs:**
+ - `ClusterExtension` - declares desired extension installations
+ - `ClusterExtensionRevision` - revision management (experimental)
+ - `ClusterCatalog` - catalog source definitions
+- **API domain:** `olm.operatorframework.io`
+ - This is the API group of our user-facing CRDs
+ - This is also the domain that should be used in ALL label and annotation prefixes that are generated by OLMv1)
+- **API version:** `v1`
+
+### Feature Gates
+
+Two manifest variants exist:
+- **Standard:** Production-ready features
+- **Experimental:** Features under development/testing (includes `ClusterExtensionRevision` API)
+
+---
+
+## Git Workflows
+
+### Branching Strategy
+
+- **Main branch:** `main` (default, protected)
+- **Release branches:** `release-v{MAJOR}.{MINOR}` (e.g., `release-v1.2`)
+- **Feature branches:** Created from `main`, usually in forks, merged via PR
+
+### Commit Message Format
+
+```
+
+
+
+```
+
+### PR Requirements
+
+- Must pass all CI checks (unit-test, e2e, sanity, lint)
+- Must have both `approved` and `lgtm` labels (from repository approvers and reviewers)
+- DCO sign-off required (Developer Certificate of Origin)
+- Reasonable title and description
+- Draft PRs: prefix with "WIP:" or use GitHub draft feature
+- PR title must use specific prefix based on the type of the PR:
+ - ⚠ (:warning:, major/breaking change)
+ - ✨ (:sparkles:, minor/compatible change)
+ - 🐛 (:bug:, patch/bug fix)
+ - 📖 (:book:, docs)
+ - 🌱 (:seedling:, other)
+
+### CI Workflows
+
+- `unit-test.yaml` - Unit tests with coverage
+- `e2e.yaml` - Multiple e2e test suites (7 variants)
+- `sanity.yaml` - Verification, linting, helm linting
+- `test-regression.yaml` - Regression tests
+- `go-apidiff.yaml` - API compatibility checks
+- `crd-diff.yaml` - CRD compatibility verification
+- `release.yaml` - Automated releases on tags
+
+### Release Process
+
+- **Semantic versioning:** `vMAJOR.MINOR.PATCH`
+- Tags trigger automated release via goreleaser
+- Patch releases from `release-v*` branches
+- Major/minor releases from `main` branch
+- Creates multi-arch container images
+- Generates release manifests and install scripts
+
+---
+
+## Boundaries (What Not to Touch)
+
+### Generated Files (require special process)
+
+**Schema Files:**
+- `/api/v1/zz_generated.deepcopy.go` - Generated by controller-gen
+- `/helm/olmv1/base/*/crd/standard/*.yaml` - Standard CRDs (generated)
+- `/helm/olmv1/base/*/crd/experimental/*.yaml` - Experimental CRDs (generated)
+- **Process:** Modify types in `/api/v1/*_types.go`, then run `make manifests`
+
+**Generated Manifests:**
+- `/manifests/standard.yaml` - Generated by `make manifests`
+- `/manifests/experimental.yaml` - Generated by `make manifests`
+- `/manifests/standard-e2e.yaml` - Generated by `make manifests`
+- `/manifests/experimental-e2e.yaml` - Generated by `make manifests`
+- **Process:** Modify Helm charts in `/helm/olmv1/`, then run `make manifests
+
+**Generated Docs:**
+- `/docs/api-reference/olmv1-api-reference.md` - Generated by `make crd-ref-docs`
+- **Process:** Requires regeneration whenever API definitions change (`api/*/*_types.go`)
+
+### CI/CD & Project Metadata
+
+**Never modify without explicit permission:**
+- `/.github/workflows/*.yaml` - CI pipelines (consult team)
+- `/.goreleaser.yml` - Release configuration
+- `/Makefile` - Core build logic (discuss changes first)
+- `/go.mod` - Dependencies (use `make tidy`, avoid Go version bumps without discussion)
+- `/PROJECT` - Kubebuilder project config
+- `/OWNERS` & `/OWNERS_ALIASES` - Maintainer lists
+- `/CODEOWNERS` - Code ownership
+- `/mkdocs.yml` - Documentation site config
+- `/.bingo/*.mod` - Tool dependencies (managed by bingo)
+- `/.golangci.yaml` - Linter configuration
+- `/kind-config.yaml` - Kind cluster config
+
+### Helm Charts (requires careful review)
+
+- `/helm/olmv1/Chart.yaml` - Chart metadata
+- `/helm/olmv1/values.yaml` - Default values
+- `/helm/olmv1/templates/*.yaml` - Chart templates
+
+### Security & Compliance
+
+- `/DCO` - Developer Certificate of Origin
+- `/LICENSE` - Apache 2.0 license
+- `/codecov.yml` - Code coverage config
+
+---
+
+## Important Notes for AI Agents
+
+1. **Never commit to `main` directly** - always use PRs
+2. **CRD changes** require running `make update-crds` and may break compatibility
+3. **API changes** in `/api/v1/` trigger CRD and CRD reference docs regeneration
+4. **Generated files** must be committed after running generators
+5. **Helm changes** require `make manifests` to update manifests
+6. **Go version changes** need community discussion (see CONTRIBUTING.md)
+7. **Dependencies:** 2-week cooldown policy for non-critical updates
+
+### Development Workflow
+
+1. Make changes to source code
+2. Run `make verify` to ensure generated code is updated
+3. Run `make lint` and `make test-unit`
+4. Commit both source and generated files
+5. CI will verify everything is in sync
+
+### Key Components to Understand
+
+**operator-controller:**
+- `ClusterExtension` controller - manages extension installations
+- `ClusterExtensionRevision` controller - manages revision lifecycle
+- Resolver - bundle version selection
+- Applier - applies manifests to cluster
+- Content Manager - manages extension content
+
+**catalogd:**
+- Catalog controllers - manage catalog unpacking
+- Storage - catalog storage backend
+- Server utilities - HTTP server for catalog content
+
+---
+
+**Last Updated:** 2025-12-10
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 1776b9b65..3d1381689 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -1,7 +1,10 @@
aliases:
olmv1-approvers:
+ - grokspawn
- joelanford
- kevinrizza
+ - oceanc80
+ - pedjak
- perdasilva
- tmshort
diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go
index 6de62b0e1..8c99cf67b 100644
--- a/api/v1/clusterextension_types.go
+++ b/api/v1/clusterextension_types.go
@@ -176,12 +176,14 @@ type ClusterExtensionConfig struct {
// inline contains JSON or YAML values specified directly in the
// ClusterExtension.
//
- // inline must be set if configType is 'Inline'.
- // inline accepts arbitrary JSON/YAML objects.
- // inline is validation at runtime against the schema provided by the bundle if a schema is provided.
+ // inline is used to specify arbitrary configuration values for the ClusterExtension.
+ // It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
+ // The configuration values are validated at runtime against a JSON schema provided by the bundle.
//
// +kubebuilder:validation:Type=object
+ // +kubebuilder:validation:MinProperties=1
// +optional
+ // +unionMember
Inline *apiextensionsv1.JSON `json:"inline,omitempty"`
}
@@ -470,6 +472,21 @@ type BundleMetadata struct {
Version string `json:"version"`
}
+// RevisionStatus defines the observed state of a ClusterExtensionRevision.
+type RevisionStatus struct {
+ // name of the ClusterExtensionRevision resource
+ Name string `json:"name"`
+ // conditions optionally expose Progressing and Available condition of the revision,
+ // in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
+ // Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
+ // to get more insights about reasons for its current state.
+ //
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
// ClusterExtensionStatus defines the observed state of a ClusterExtension.
type ClusterExtensionStatus struct {
// The set of condition types which apply to all spec.source variations are Installed and Progressing.
@@ -482,6 +499,9 @@ type ClusterExtensionStatus struct {
// When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state.
// When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
// When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
+ //
+ // When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
+ //
//
// When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
// These are indications from a package owner to guide users away from a particular package, channel, or bundle.
@@ -499,6 +519,14 @@ type ClusterExtensionStatus struct {
//
// +optional
Install *ClusterExtensionInstallStatus `json:"install,omitempty"`
+
+ // activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
+ // including both installed and rolling out revisions.
+ // +listType=map
+ // +listMapKey=name
+ // +optional
+ //
+ ActiveRevisions []RevisionStatus `json:"activeRevisions,omitempty"`
}
// ClusterExtensionInstallStatus is a representation of the status of the identified bundle.
diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go
index 69a116300..368a8fca8 100644
--- a/api/v1/clusterextensionrevision_types.go
+++ b/api/v1/clusterextensionrevision_types.go
@@ -24,26 +24,18 @@ import (
const (
ClusterExtensionRevisionKind = "ClusterExtensionRevision"
- // ClusterExtensionRevisionTypeAvailable is the condition type that represents whether the
- // ClusterExtensionRevision is available and has been successfully rolled out.
- ClusterExtensionRevisionTypeAvailable = "Available"
-
- // ClusterExtensionRevisionTypeSucceeded is the condition type that represents whether the
- // ClusterExtensionRevision rollout has succeeded.
- ClusterExtensionRevisionTypeSucceeded = "Succeeded"
-
- // Condition reasons
- ClusterExtensionRevisionReasonAvailable = "Available"
- ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure"
- ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure"
- ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError"
- ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions"
- ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess"
- ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure"
- ClusterExtensionRevisionReasonIncomplete = "Incomplete"
- ClusterExtensionRevisionReasonProgressing = "Progressing"
- ClusterExtensionRevisionReasonArchived = "Archived"
- ClusterExtensionRevisionReasonMigrated = "Migrated"
+ // Condition Types
+ ClusterExtensionRevisionTypeAvailable = "Available"
+ ClusterExtensionRevisionTypeProgressing = "Progressing"
+ ClusterExtensionRevisionTypeSucceeded = "Succeeded"
+
+ // Condition Reasons
+ ClusterExtensionRevisionReasonArchived = "Archived"
+ ClusterExtensionRevisionReasonBlocked = "Blocked"
+ ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure"
+ ClusterExtensionRevisionReasonProbesSucceeded = "ProbesSucceeded"
+ ClusterExtensionRevisionReasonReconciling = "Reconciling"
+ ClusterExtensionRevisionReasonRetrying = "Retrying"
)
// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision.
@@ -103,9 +95,6 @@ type ClusterExtensionRevisionLifecycleState string
const (
// ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state.
ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active"
- // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision.
- // Object changes will not be reconciled. However, status updates will be propagated.
- ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused"
// ClusterExtensionRevisionLifecycleStateArchived / "Archived" archives the revision for historical or auditing purposes.
// The revision is removed from the owner list of all other objects previously under management and all objects
// that did not transition to a succeeding revision are deleted.
@@ -190,22 +179,21 @@ type ClusterExtensionRevisionStatus struct {
// ClusterExtensionRevision.
//
// The Progressing condition represents whether the revision is actively rolling out:
- // - When status is True and reason is Progressing, the revision rollout is actively making progress and is in transition.
- // - When Progressing is not present, the revision is not currently in transition.
+ // - When status is True and reason is RollingOut, the ClusterExtensionRevision rollout is actively making progress and is in transition.
+ // - When status is True and reason is Retrying, the ClusterExtensionRevision has encountered an error that could be resolved on subsequent reconciliation attempts.
+ // - When status is True and reason is Succeeded, the ClusterExtensionRevision has reached the desired state.
+ // - When status is False and reason is Blocked, the ClusterExtensionRevision has encountered an error that requires manual intervention for recovery.
+ // - When status is False and reason is Archived, the ClusterExtensionRevision is archived and not being actively reconciled.
//
// The Available condition represents whether the revision has been successfully rolled out and is available:
- // - When status is True and reason is Available, the revision has been successfully rolled out and all objects pass their readiness probes.
- // - When status is False and reason is Incomplete, the revision rollout has not yet completed but no specific failures have been detected.
+ // - When status is True and reason is ProbesSucceeded, the ClusterExtensionRevision has been successfully rolled out and all objects pass their readiness probes.
// - When status is False and reason is ProbeFailure, one or more objects are failing their readiness probes during rollout.
- // - When status is False and reason is ReconcileFailure, the revision has encountered a general reconciliation failure.
- // - When status is False and reason is RevisionValidationFailure, the revision failed preflight validation checks.
- // - When status is False and reason is PhaseValidationError, a phase within the revision failed preflight validation checks.
- // - When status is False and reason is ObjectCollisions, objects in the revision collide with existing cluster objects that cannot be adopted.
- // - When status is Unknown and reason is Archived, the revision has been archived and its objects have been torn down.
- // - When status is Unknown and reason is Migrated, the revision was migrated from an existing release and object status probe results have not yet been observed.
+ // - When status is Unknown and reason is Reconciling, the ClusterExtensionRevision has encountered an error that prevented it from observing the probes.
+ // - When status is Unknown and reason is Archived, the ClusterExtensionRevision has been archived and its objects have been torn down.
+ // - When status is Unknown and reason is Migrated, the ClusterExtensionRevision was migrated from an existing release and object status probe results have not yet been observed.
//
// The Succeeded condition represents whether the revision has successfully completed its rollout:
- // - When status is True and reason is RolloutSuccess, the revision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
+ // - When status is True and reason is Succeeded, the ClusterExtensionRevision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
//
// +listType=map
// +listMapKey=type
@@ -217,6 +205,7 @@ type ClusterExtensionRevisionStatus struct {
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status`
+// +kubebuilder:printcolumn:name="Progressing",type=string,JSONPath=`.status.conditions[?(@.type=='Progressing')].status`
// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp`
// ClusterExtensionRevision represents an immutable snapshot of Kubernetes objects
diff --git a/api/v1/common_types.go b/api/v1/common_types.go
index 6ab5336ac..115836b10 100644
--- a/api/v1/common_types.go
+++ b/api/v1/common_types.go
@@ -24,9 +24,9 @@ const (
ReasonAbsent = "Absent"
// Progressing reasons
- ReasonRolloutInProgress = "RolloutInProgress"
- ReasonRetrying = "Retrying"
- ReasonBlocked = "Blocked"
+ ReasonRollingOut = "RollingOut"
+ ReasonRetrying = "Retrying"
+ ReasonBlocked = "Blocked"
// Deprecation reasons
ReasonDeprecated = "Deprecated"
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index cc27ec68f..0c5c67c16 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -522,6 +522,13 @@ func (in *ClusterExtensionStatus) DeepCopyInto(out *ClusterExtensionStatus) {
*out = new(ClusterExtensionInstallStatus)
**out = **in
}
+ if in.ActiveRevisions != nil {
+ in, out := &in.ActiveRevisions, &out.ActiveRevisions
+ *out = make([]RevisionStatus, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionStatus.
@@ -609,6 +616,28 @@ func (in *ResolvedImageSource) DeepCopy() *ResolvedImageSource {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RevisionStatus) DeepCopyInto(out *RevisionStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]metav1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RevisionStatus.
+func (in *RevisionStatus) DeepCopy() *RevisionStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(RevisionStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceAccountReference) DeepCopyInto(out *ServiceAccountReference) {
*out = *in
diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go
index c72ba60f2..a1d03feee 100644
--- a/cmd/operator-controller/main.go
+++ b/cmd/operator-controller/main.go
@@ -628,7 +628,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
- controllers.ApplyBundle(appl),
+ controllers.ApplyBundleWithBoxcutter(appl),
}
baseDiscoveryClient, err := discovery.NewDiscoveryClientForConfig(c.mgr.GetConfig())
diff --git a/commitchecker.yaml b/commitchecker.yaml
index 6bb0b5fac..652ace66d 100644
--- a/commitchecker.yaml
+++ b/commitchecker.yaml
@@ -1,4 +1,4 @@
-expectedMergeBase: 4e349e62c5314574f6194d64b1ff4508f2e9331f
+expectedMergeBase: dcb1c7ffad3207033bde496f8af00ac7c2ad65dc
upstreamBranch: main
upstreamOrg: operator-framework
upstreamRepo: operator-controller
diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md
index 317b46a00..866de5327 100644
--- a/docs/api-reference/olmv1-api-reference.md
+++ b/docs/api-reference/olmv1-api-reference.md
@@ -254,7 +254,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `configType` _[ClusterExtensionConfigType](#clusterextensionconfigtype)_ | configType is a required reference to the type of configuration source.
Allowed values are "Inline"
When this field is set to "Inline", the cluster extension configuration is defined inline within the
ClusterExtension resource. | | Enum: [Inline]
Required: \{\}
|
-| `inline` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#json-v1-apiextensions-k8s-io)_ | inline contains JSON or YAML values specified directly in the
ClusterExtension.
inline must be set if configType is 'Inline'.
inline accepts arbitrary JSON/YAML objects.
inline is validation at runtime against the schema provided by the bundle if a schema is provided. | | Type: object
|
+| `inline` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#json-v1-apiextensions-k8s-io)_ | inline contains JSON or YAML values specified directly in the
ClusterExtension.
inline is used to specify arbitrary configuration values for the ClusterExtension.
It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
The configuration values are validated at runtime against a JSON schema provided by the bundle. | | MinProperties: 1
Type: object
|
#### ClusterExtensionConfigType
@@ -359,8 +359,9 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
-| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | The set of condition types which apply to all spec.source variations are Installed and Progressing.
The Installed condition represents whether or not the bundle has been installed for this ClusterExtension.
When Installed is True and the Reason is Succeeded, the bundle has been successfully installed.
When Installed is False and the Reason is Failed, the bundle has failed to install.
The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state.
When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state.
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
ChannelDeprecated is set if the requested channel is marked deprecated in the catalog.
PackageDeprecated is set if the requested package is marked deprecated in the catalog.
Deprecated is a rollup condition that is present when any of the deprecated conditions are present. | | |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | The set of condition types which apply to all spec.source variations are Installed and Progressing.
The Installed condition represents whether or not the bundle has been installed for this ClusterExtension.
When Installed is True and the Reason is Succeeded, the bundle has been successfully installed.
When Installed is False and the Reason is Failed, the bundle has failed to install.
The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state.
When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state.
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
ChannelDeprecated is set if the requested channel is marked deprecated in the catalog.
PackageDeprecated is set if the requested package is marked deprecated in the catalog.
Deprecated is a rollup condition that is present when any of the deprecated conditions are present. | | |
| `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | |
+| `activeRevisions` _[RevisionStatus](#revisionstatus) array_ | activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
including both installed and rolling out revisions.
| | |
@@ -435,6 +436,23 @@ _Appears in:_
| `ref` _string_ | ref contains the resolved image digest-based reference.
The digest format is used so users can use other tooling to fetch the exact
OCI manifests that were used to extract the catalog contents. | | MaxLength: 1000
Required: \{\}
|
+#### RevisionStatus
+
+
+
+RevisionStatus defines the observed state of a ClusterExtensionRevision.
+
+
+
+_Appears in:_
+- [ClusterExtensionStatus](#clusterextensionstatus)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | name of the ClusterExtensionRevision resource | | |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions optionally expose Progressing and Available condition of the revision,
in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
to get more insights about reasons for its current state. | | |
+
+
#### ServiceAccountReference
diff --git a/docs/contribute/developer.md b/docs/contribute/developer.md
index d33b98f20..909b653c5 100644
--- a/docs/contribute/developer.md
+++ b/docs/contribute/developer.md
@@ -59,7 +59,7 @@ make manifests
!!! note
Run `make help` for more information on all potential `make` targets.
-### Rapid Iterative Development with Tilt
+## Rapid Iterative Development with Tilt
If you are developing against the combined ecosystem of catalogd + operator-controller, you will want to take advantage of `tilt`:
@@ -135,6 +135,36 @@ Shortly after starting, Tilt processes the `Tiltfile`, resulting in:
- Modifying the Deployments to use the just-built images
- Creating the Deployments
+### Using the Claude local development environment sub-agent
+
+The repository contains a configuration for a Claude sub-agent that can help walk
+you through the process of setting up your local development environment. If you are
+using Claude, simply ask it for assistance in setting up the local development environment
+and it should suggest using the local development environment specialist sub-agent. For example:
+
+```
+> Can you help me set up a local dev environment?
+
+● I'll help you set up the local development environment for this OLM v1 project. Since this project has a specialized sub-agent for
+ local development setup, I'll use that to guide you through the complete process.
+
+---
+
+> I need help setting up a local dev env
+
+● I'll help you set up a local development environment for the operator-controller project. Since this involves setting up a complete
+ development environment with multiple components, I'll use the specialized OLM development environment agent to guide you through the
+ process.
+```
+
+The sub-agent is designed to assist with:
+- Checking for all the necessary tooling and installing missing binaries
+- Helping with OS-specific configuration
+- Running and monitoring the kind cluster and Tilt server
+
+As with all LLM-based tooling, always use good judgement and verify its suggestions. This is not
+intended as a substitute for the concrete instructions in the rest of the project documentation.
+
---
## Special Setup for MacOS
diff --git a/docs/project/olmv1_design_decisions.md b/docs/project/olmv1_design_decisions.md
index 6051bc2d8..ed9a16b79 100644
--- a/docs/project/olmv1_design_decisions.md
+++ b/docs/project/olmv1_design_decisions.md
@@ -31,7 +31,7 @@ The Kubernetes design assumptions are:
- CRDs and their controllers are trusted cluster extensions.
- If an object for an API exists a controller WILL reconcile it, no matter where it is in the cluster.
-OLM v1 will make the same assumption that Kubernetes does and that users of Kubernetes APIs do. That is: If a user has RBAC to create an object in the cluster, they can expect that a controller exists that will reconcile that object. If this assumption does not hold, it will be considered a configuration issue, not an OLM v1 bug.
+OLM v1 will make the same assumption that Kubernetes does and that users of Kubernetes APIs do. That is: If a user has RBAC to create an object in the cluster, they can expect that exactly one controller exists that will reconcile that object. If this assumption does not hold, it will be considered a configuration issue, not an OLM v1 bug.
This means that it is a best practice to implement and configure controllers to have cluster-wide permission to read and update the status of their primary APIs. It does not mean that a controller needs cluster-wide access to read/write secondary APIs. If a controller can update the status of its primary APIs, it can tell users when it lacks permission to act on secondary APIs.
diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml
index 1a3a8b021..c448a4da4 100644
--- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml
+++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml
@@ -19,6 +19,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=='Available')].status
name: Available
type: string
+ - jsonPath: .status.conditions[?(@.type=='Progressing')].status
+ name: Progressing
+ type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
@@ -190,22 +193,21 @@ spec:
ClusterExtensionRevision.
The Progressing condition represents whether the revision is actively rolling out:
- - When status is True and reason is Progressing, the revision rollout is actively making progress and is in transition.
- - When Progressing is not present, the revision is not currently in transition.
+ - When status is True and reason is RollingOut, the ClusterExtensionRevision rollout is actively making progress and is in transition.
+ - When status is True and reason is Retrying, the ClusterExtensionRevision has encountered an error that could be resolved on subsequent reconciliation attempts.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has reached the desired state.
+ - When status is False and reason is Blocked, the ClusterExtensionRevision has encountered an error that requires manual intervention for recovery.
+ - When status is False and reason is Archived, the ClusterExtensionRevision is archived and not being actively reconciled.
The Available condition represents whether the revision has been successfully rolled out and is available:
- - When status is True and reason is Available, the revision has been successfully rolled out and all objects pass their readiness probes.
- - When status is False and reason is Incomplete, the revision rollout has not yet completed but no specific failures have been detected.
+ - When status is True and reason is ProbesSucceeded, the ClusterExtensionRevision has been successfully rolled out and all objects pass their readiness probes.
- When status is False and reason is ProbeFailure, one or more objects are failing their readiness probes during rollout.
- - When status is False and reason is ReconcileFailure, the revision has encountered a general reconciliation failure.
- - When status is False and reason is RevisionValidationFailure, the revision failed preflight validation checks.
- - When status is False and reason is PhaseValidationError, a phase within the revision failed preflight validation checks.
- - When status is False and reason is ObjectCollisions, objects in the revision collide with existing cluster objects that cannot be adopted.
- - When status is Unknown and reason is Archived, the revision has been archived and its objects have been torn down.
- - When status is Unknown and reason is Migrated, the revision was migrated from an existing release and object status probe results have not yet been observed.
+ - When status is Unknown and reason is Reconciling, the ClusterExtensionRevision has encountered an error that prevented it from observing the probes.
+ - When status is Unknown and reason is Archived, the ClusterExtensionRevision has been archived and its objects have been torn down.
+ - When status is Unknown and reason is Migrated, the ClusterExtensionRevision was migrated from an existing release and object status probe results have not yet been observed.
The Succeeded condition represents whether the revision has successfully completed its rollout:
- - When status is True and reason is RolloutSuccess, the revision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml
index 1038b7fdf..b51817c16 100644
--- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml
+++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml
@@ -83,9 +83,10 @@ spec:
inline contains JSON or YAML values specified directly in the
ClusterExtension.
- inline must be set if configType is 'Inline'.
- inline accepts arbitrary JSON/YAML objects.
- inline is validation at runtime against the schema provided by the bundle if a schema is provided.
+ inline is used to specify arbitrary configuration values for the ClusterExtension.
+ It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
+ The configuration values are validated at runtime against a JSON schema provided by the bundle.
+ minProperties: 1
type: object
x-kubernetes-preserve-unknown-fields: true
required:
@@ -505,6 +506,88 @@ spec:
description: status is an optional field that defines the observed state
of the ClusterExtension.
properties:
+ activeRevisions:
+ description: |-
+ activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
+ including both installed and rolling out revisions.
+ items:
+ description: RevisionStatus defines the observed state of a ClusterExtensionRevision.
+ properties:
+ conditions:
+ description: |-
+ conditions optionally expose Progressing and Available condition of the revision,
+ in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
+ Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
+ to get more insights about reasons for its current state.
+ items:
+ description: Condition contains details for one aspect of
+ the current state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ name:
+ description: name of the ClusterExtensionRevision resource
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
The set of condition types which apply to all spec.source variations are Installed and Progressing.
@@ -518,6 +601,8 @@ spec:
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
+ When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
+
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go
index 3895b49df..c69ab5a1a 100644
--- a/internal/operator-controller/applier/boxcutter.go
+++ b/internal/operator-controller/applier/boxcutter.go
@@ -266,20 +266,6 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste
return fmt.Errorf("getting created revision: %w", err)
}
- // Set Available=Unknown so the revision controller will verify cluster state through probes.
- // During migration, ClusterExtension will briefly show as not installed until verification completes.
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionUnknown,
- Reason: ocv1.ClusterExtensionRevisionReasonMigrated,
- Message: "Migrated from Helm storage, awaiting cluster state verification",
- ObservedGeneration: rev.Generation,
- })
-
- if err := m.Client.Status().Update(ctx, rev); err != nil {
- return fmt.Errorf("updating migrated revision status: %w", err)
- }
-
return nil
}
@@ -405,9 +391,14 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
if progressingCondition == nil && availableCondition == nil && succeededCondition == nil {
return false, "New revision created", nil
} else if progressingCondition != nil && progressingCondition.Status == metav1.ConditionTrue {
- return false, progressingCondition.Message, nil
- } else if availableCondition != nil && availableCondition.Status != metav1.ConditionTrue {
- return false, "", errors.New(availableCondition.Message)
+ switch progressingCondition.Reason {
+ case ocv1.ReasonSucceeded:
+ return true, "", nil
+ case ocv1.ClusterExtensionRevisionReasonRetrying:
+ return false, "", errors.New(progressingCondition.Message)
+ default:
+ return false, progressingCondition.Message, nil
+ }
} else if succeededCondition != nil && succeededCondition.Status != metav1.ConditionTrue {
return false, succeededCondition.Message, nil
}
diff --git a/internal/operator-controller/conditionsets/conditionsets.go b/internal/operator-controller/conditionsets/conditionsets.go
index 0d63e1abb..6c33b1c8f 100644
--- a/internal/operator-controller/conditionsets/conditionsets.go
+++ b/internal/operator-controller/conditionsets/conditionsets.go
@@ -40,5 +40,5 @@ var ConditionReasons = []string{
ocv1.ReasonBlocked,
ocv1.ReasonRetrying,
ocv1.ReasonAbsent,
- ocv1.ReasonRolloutInProgress,
+ ocv1.ReasonRollingOut,
}
diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go
index 78e3ef132..01bb2232d 100644
--- a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go
+++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go
@@ -25,6 +25,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/log"
ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
@@ -50,11 +51,7 @@ func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, e
rs := &RevisionStates{}
for _, rev := range existingRevisionList.Items {
- switch rev.Spec.LifecycleState {
- case ocv1.ClusterExtensionRevisionLifecycleStateActive,
- ocv1.ClusterExtensionRevisionLifecycleStatePaused:
- default:
- // Skip anything not active or paused, which should only be "Archived".
+ if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived {
continue
}
@@ -62,8 +59,10 @@ func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, e
// is fairly decoupled from this code where we get the annotations back out. We may want to co-locate
// the set/get logic a bit better to make it more maintainable and less likely to get out of sync.
rm := &RevisionMetadata{
- Package: rev.Annotations[labels.PackageNameKey],
- Image: rev.Annotations[labels.BundleReferenceKey],
+ RevisionName: rev.Name,
+ Package: rev.Annotations[labels.PackageNameKey],
+ Image: rev.Annotations[labels.BundleReferenceKey],
+ Conditions: rev.Status.Conditions,
BundleMetadata: ocv1.BundleMetadata{
Name: rev.Annotations[labels.BundleNameKey],
Version: rev.Annotations[labels.BundleVersionKey],
@@ -93,3 +92,61 @@ func MigrateStorage(m StorageMigrator) ReconcileStepFunc {
return nil, nil
}
}
+
+func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc {
+ return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
+ l := log.FromContext(ctx)
+ revisionAnnotations := map[string]string{
+ labels.BundleNameKey: state.resolvedRevisionMetadata.Name,
+ labels.PackageNameKey: state.resolvedRevisionMetadata.Package,
+ labels.BundleVersionKey: state.resolvedRevisionMetadata.Version,
+ labels.BundleReferenceKey: state.resolvedRevisionMetadata.Image,
+ }
+ objLbls := map[string]string{
+ labels.OwnerKindKey: ocv1.ClusterExtensionKind,
+ labels.OwnerNameKey: ext.GetName(),
+ }
+
+ l.Info("applying bundle contents")
+ if _, _, err := a.Apply(ctx, state.imageFS, ext, objLbls, revisionAnnotations); err != nil {
+ // If there was an error applying the resolved bundle,
+ // report the error via the Progressing condition.
+ setStatusProgressing(ext, wrapErrorWithResolutionInfo(state.resolvedRevisionMetadata.BundleMetadata, err))
+ return nil, err
+ }
+
+ // Mirror Available/Progressing conditions from the installed revision
+ if i := state.revisionStates.Installed; i != nil {
+ for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} {
+ if cnd := apimeta.FindStatusCondition(i.Conditions, cndType); cnd != nil {
+ cnd.ObservedGeneration = ext.GetGeneration()
+ apimeta.SetStatusCondition(&ext.Status.Conditions, *cnd)
+ }
+ }
+ ext.Status.Install = &ocv1.ClusterExtensionInstallStatus{
+ Bundle: i.BundleMetadata,
+ }
+ ext.Status.ActiveRevisions = []ocv1.RevisionStatus{{Name: i.RevisionName}}
+ }
+ for idx, r := range state.revisionStates.RollingOut {
+ rs := ocv1.RevisionStatus{Name: r.RevisionName}
+ for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} {
+ if cnd := apimeta.FindStatusCondition(r.Conditions, cndType); cnd != nil {
+ cnd.ObservedGeneration = ext.GetGeneration()
+ apimeta.SetStatusCondition(&rs.Conditions, *cnd)
+ }
+ }
+ // Mirror Progressing condition from the latest active revision
+ if idx == len(state.revisionStates.RollingOut)-1 {
+ if pcnd := apimeta.FindStatusCondition(r.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing); pcnd != nil {
+ pcnd.ObservedGeneration = ext.GetGeneration()
+ apimeta.SetStatusCondition(&ext.Status.Conditions, *pcnd)
+ }
+ }
+ ext.Status.ActiveRevisions = append(ext.Status.ActiveRevisions, rs)
+ }
+
+ setInstalledStatusFromRevisionStates(ext, state.revisionStates)
+ return nil, nil
+ }
+}
diff --git a/internal/operator-controller/controllers/clusterextension_admission_test.go b/internal/operator-controller/controllers/clusterextension_admission_test.go
index 38c6c60d4..2f8791999 100644
--- a/internal/operator-controller/controllers/clusterextension_admission_test.go
+++ b/internal/operator-controller/controllers/clusterextension_admission_test.go
@@ -462,9 +462,18 @@ func Test_ClusterExtensionAdmissionInlineConfig(t *testing.T) {
errMsg: "spec.config.inline in body must be of type object",
},
{
- name: "accepts valid json object",
+ name: "rejects empty json object",
+ configBytes: []byte(`{}`),
+ errMsg: "spec.config.inline in body should have at least 1 properties",
+ },
+ {
+ name: "accepts valid json object with configuration",
configBytes: []byte(`{"key": "value"}`),
},
+ {
+ name: "accepts valid json object with nested configuration",
+ configBytes: []byte(`{"key": {"foo": ["bar", "baz"], "h4x0r": 1337}}`),
+ },
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go
index 2b2f0d532..7f3192b0f 100644
--- a/internal/operator-controller/controllers/clusterextension_controller.go
+++ b/internal/operator-controller/controllers/clusterextension_controller.go
@@ -332,9 +332,11 @@ func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) crh
}
type RevisionMetadata struct {
- Package string
- Image string
+ RevisionName string
+ Package string
+ Image string
ocv1.BundleMetadata
+ Conditions []metav1.Condition
}
type RevisionStates struct {
diff --git a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go
index 2e1e9232a..c4cdbdddc 100644
--- a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go
+++ b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go
@@ -191,7 +191,7 @@ func ApplyBundle(a Applier) ReconcileStepFunc {
apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{
Type: ocv1.TypeProgressing,
Status: metav1.ConditionTrue,
- Reason: ocv1.ReasonRolloutInProgress,
+ Reason: ocv1.ReasonRollingOut,
Message: rolloutStatus,
ObservedGeneration: ext.GetGeneration(),
})
diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go
index ec035eee7..f9c11c2ad 100644
--- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go
+++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go
@@ -117,13 +117,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
revision, opts, err := c.toBoxcutterRevision(ctx, rev)
if err != nil {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
+ setRetryingConditions(rev, err.Error())
return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err)
}
@@ -131,77 +125,42 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
return c.teardown(ctx, rev, revision)
}
+ revVersion := rev.GetAnnotations()[labels.BundleVersionKey]
//
// Reconcile
//
if err := c.ensureFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
return ctrl.Result{}, fmt.Errorf("error ensuring teardown finalizer: %v", err)
}
if err := c.establishWatch(ctx, rev, revision); err != nil {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
- return ctrl.Result{}, fmt.Errorf("establish watch: %v", err)
+ werr := fmt.Errorf("establish watch: %v", err)
+ setRetryingConditions(rev, werr.Error())
+ return ctrl.Result{}, werr
}
rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...)
if err != nil {
if rres != nil {
- l.Error(err, "revision reconcile failed")
- l.V(1).Info("reconcile failure report", "report", rres.String())
- } else {
- l.Error(err, "revision reconcile failed")
+ // Log detailed reconcile reports only in debug mode (V(1)) to reduce verbosity.
+ l.V(1).Info("reconcile report", "report", rres.String())
}
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
+ setRetryingConditions(rev, err.Error())
return ctrl.Result{}, fmt.Errorf("revision reconcile: %v", err)
}
- // Log detailed reconcile reports only in debug mode (V(1)) to reduce verbosity.
- l.V(1).Info("reconcile report", "report", rres.String())
// Retry failing preflight checks with a flat 10s retry.
// TODO: report status, backoff?
if verr := rres.GetValidationError(); verr != nil {
l.Error(fmt.Errorf("%w", verr), "preflight validation failed, retrying after 10s")
-
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonRevisionValidationFailure,
- Message: fmt.Sprintf("revision validation error: %s", verr),
- ObservedGeneration: rev.Generation,
- })
+ setRetryingConditions(rev, fmt.Sprintf("revision validation error: %s", verr))
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
for i, pres := range rres.GetPhases() {
if verr := pres.GetValidationError(); verr != nil {
l.Error(fmt.Errorf("%w", verr), "phase preflight validation failed, retrying after 10s", "phase", i)
-
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonPhaseValidationError,
- Message: fmt.Sprintf("phase %d validation error: %s", i, verr),
- ObservedGeneration: rev.Generation,
- })
+ setRetryingConditions(rev, fmt.Sprintf("phase %d validation error: %s", i, verr))
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
@@ -214,18 +173,17 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
if len(collidingObjs) > 0 {
l.Error(fmt.Errorf("object collision detected"), "object collision, retrying after 10s", "phase", i, "collisions", collidingObjs)
-
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonObjectCollisions,
- Message: fmt.Sprintf("revision object collisions in phase %d\n%s", i, strings.Join(collidingObjs, "\n\n")),
- ObservedGeneration: rev.Generation,
- })
+ setRetryingConditions(rev, fmt.Sprintf("revision object collisions in phase %d\n%s", i, strings.Join(collidingObjs, "\n\n")))
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
}
+ if !rres.InTransistion() {
+ markAsProgressing(rev, ocv1.ReasonSucceeded, fmt.Sprintf("Revision %s has rolled out.", revVersion))
+ } else {
+ markAsProgressing(rev, ocv1.ReasonRollingOut, fmt.Sprintf("Revision %s is rolling out.", revVersion))
+ }
+
//nolint:nestif
if rres.IsComplete() {
// Archive previous revisions
@@ -243,23 +201,18 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
}
}
- // Report status.
+ markAsAvailable(rev, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, "Objects are available and pass all probes.")
+
+ // We'll probably only want to remove this once we are done updating the ClusterExtension conditions
+ // as its one of the interfaces between the revision and the extension. If we still have the Succeeded for now
+ // that's fine.
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
+ Type: ocv1.ClusterExtensionRevisionTypeSucceeded,
Status: metav1.ConditionTrue,
- Reason: ocv1.ClusterExtensionRevisionReasonAvailable,
- Message: "Object is available and passes all probes.",
+ Reason: ocv1.ReasonSucceeded,
+ Message: "Revision succeeded rolling out.",
ObservedGeneration: rev.Generation,
})
- if !meta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeSucceeded,
- Status: metav1.ConditionTrue,
- Reason: ocv1.ClusterExtensionRevisionReasonRolloutSuccess,
- Message: "Revision succeeded rolling out.",
- ObservedGeneration: rev.Generation,
- })
- }
} else {
var probeFailureMsgs []string
for _, pres := range rres.GetPhases() {
@@ -267,6 +220,8 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
continue
}
for _, ores := range pres.GetObjects() {
+ // we probably want an AvailabilityProbeType and run through all of them independently of whether
+ // the revision is complete or not
pr := ores.Probes()[boxcutter.ProgressProbeType]
if pr.Success {
continue
@@ -274,6 +229,8 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
obj := ores.Object()
gvk := obj.GetObjectKind().GroupVersionKind()
+ // I think these can be pretty large and verbose. We may want to
+ // work a little on the formatting...?
probeFailureMsgs = append(probeFailureMsgs, fmt.Sprintf(
"Object %s.%s %s/%s: %v",
gvk.Kind, gvk.GroupVersion().String(),
@@ -282,35 +239,13 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
break
}
}
+
if len(probeFailureMsgs) > 0 {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonProbeFailure,
- Message: strings.Join(probeFailureMsgs, "\n"),
- ObservedGeneration: rev.Generation,
- })
+ markAsUnavailable(rev, ocv1.ClusterExtensionRevisionReasonProbeFailure, strings.Join(probeFailureMsgs, "\n"))
} else {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonIncomplete,
- Message: "Revision has not been rolled out completely.",
- ObservedGeneration: rev.Generation,
- })
+ markAsUnavailable(rev, ocv1.ReasonRollingOut, fmt.Sprintf("Revision %s is rolling out.", revVersion))
}
}
- if rres.InTransistion() {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.TypeProgressing,
- Status: metav1.ConditionTrue,
- Reason: ocv1.ClusterExtensionRevisionReasonProgressing,
- Message: "Rollout in progress.",
- ObservedGeneration: rev.Generation,
- })
- } else {
- meta.RemoveStatusCondition(&rev.Status.Conditions, ocv1.TypeProgressing)
- }
return ctrl.Result{}, nil
}
@@ -321,18 +256,9 @@ func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *
tres, err := c.RevisionEngine.Teardown(ctx, *revision)
if err != nil {
if tres != nil {
- l.Error(err, "revision teardown failed")
l.V(1).Info("teardown failure report", "report", tres.String())
- } else {
- l.Error(err, "revision teardown failed")
}
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
+ markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error())
return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err)
}
@@ -346,26 +272,12 @@ func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *
}
if err := c.TrackingCache.Free(ctx, rev); err != nil {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionFalse,
- Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure,
- Message: err.Error(),
- ObservedGeneration: rev.Generation,
- })
+ markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error())
return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err)
}
- // Ensure Available condition is set to Unknown before removing the finalizer when archiving
- if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived &&
- !meta.IsStatusConditionPresentAndEqual(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable, metav1.ConditionUnknown) {
- meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
- Type: ocv1.ClusterExtensionRevisionTypeAvailable,
- Status: metav1.ConditionUnknown,
- Reason: ocv1.ClusterExtensionRevisionReasonArchived,
- Message: "revision is archived",
- ObservedGeneration: rev.Generation,
- })
+ // Ensure conditions are set before removing the finalizer when archiving
+ if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived && markAsArchived(rev) {
return ctrl.Result{}, nil
}
@@ -538,10 +450,6 @@ func (c *ClusterExtensionRevisionReconciler) toBoxcutterRevision(ctx context.Con
}
r.Phases = append(r.Phases, phase)
}
-
- if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStatePaused {
- opts = append(opts, boxcutter.WithPaused{})
- }
return r, opts, nil
}
@@ -605,3 +513,66 @@ var (
FieldB: ".status.replicas",
}
)
+
+func setRetryingConditions(cer *ocv1.ClusterExtensionRevision, message string) {
+ markAsProgressing(cer, ocv1.ClusterExtensionRevisionReasonRetrying, message)
+ if meta.FindStatusCondition(cer.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) != nil {
+ markAsAvailableUnknown(cer, ocv1.ClusterExtensionRevisionReasonReconciling, message)
+ }
+}
+
+func markAsProgressing(cer *ocv1.ClusterExtensionRevision, reason, message string) {
+ meta.SetStatusCondition(&cer.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeProgressing,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: cer.Generation,
+ })
+}
+
+func markAsNotProgressing(cer *ocv1.ClusterExtensionRevision, reason, message string) bool {
+ return meta.SetStatusCondition(&cer.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeProgressing,
+ Status: metav1.ConditionFalse,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: cer.Generation,
+ })
+}
+
+func markAsAvailable(cer *ocv1.ClusterExtensionRevision, reason, message string) bool {
+ return meta.SetStatusCondition(&cer.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeAvailable,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: cer.Generation,
+ })
+}
+
+func markAsUnavailable(cer *ocv1.ClusterExtensionRevision, reason, message string) {
+ meta.SetStatusCondition(&cer.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeAvailable,
+ Status: metav1.ConditionFalse,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: cer.Generation,
+ })
+}
+
+func markAsAvailableUnknown(cer *ocv1.ClusterExtensionRevision, reason, message string) bool {
+ return meta.SetStatusCondition(&cer.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeAvailable,
+ Status: metav1.ConditionUnknown,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: cer.Generation,
+ })
+}
+
+func markAsArchived(cer *ocv1.ClusterExtensionRevision) bool {
+ const msg = "revision is archived"
+ updated := markAsNotProgressing(cer, ocv1.ClusterExtensionRevisionReasonArchived, msg)
+ return markAsAvailableUnknown(cer, ocv1.ClusterExtensionRevisionReasonArchived, msg) || updated
+}
diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go
index e88051537..e54827962 100644
--- a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go
+++ b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go
@@ -2,16 +2,18 @@ package controllers_test
import (
"context"
+ "errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/errors"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
@@ -32,26 +34,26 @@ import (
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
)
-func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *testing.T) {
- const (
- clusterExtensionRevisionName = "test-ext-1"
- )
+const clusterExtensionRevisionName = "test-ext-1"
+func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t *testing.T) {
testScheme := newScheme(t)
for _, tc := range []struct {
- name string
- existingObjs func() []client.Object
- revisionResult machinery.RevisionResult
- validate func(*testing.T, client.Client)
+ name string
+ reconcilingRevisionName string
+ existingObjs func() []client.Object
+ revisionResult machinery.RevisionResult
+ revisionReconcileErr error
+ validate func(*testing.T, client.Client)
}{
{
- name: "sets teardown finalizer",
- revisionResult: mockRevisionResult{},
+ name: "sets teardown finalizer",
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
return []client.Object{ext, rev1}
},
validate: func(t *testing.T, c client.Client) {
@@ -64,12 +66,154 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
},
},
{
- name: "set Available:False:InComplete status condition during rollout when no probe failures are detected",
- revisionResult: mockRevisionResult{},
+ name: "Available condition is not updated on error if its not already set",
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ revisionResult: mockRevisionResult{},
+ revisionReconcileErr: errors.New("some error"),
+ existingObjs: func() []client.Object {
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
+ return []client.Object{ext, rev1}
+ },
+ validate: func(t *testing.T, c client.Client) {
+ rev := &ocv1.ClusterExtensionRevision{}
+ err := c.Get(t.Context(), client.ObjectKey{
+ Name: clusterExtensionRevisionName,
+ }, rev)
+ require.NoError(t, err)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.Nil(t, cond)
+ },
+ },
+ {
+ name: "Available condition is updated to Unknown on error if its been already set",
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ revisionResult: mockRevisionResult{},
+ revisionReconcileErr: errors.New("some error"),
+ existingObjs: func() []client.Object {
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
+ meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeAvailable,
+ Status: metav1.ConditionTrue,
+ Reason: ocv1.ClusterExtensionRevisionReasonProbesSucceeded,
+ Message: "Revision 1.0.0 is rolled out.",
+ ObservedGeneration: 1,
+ })
+ return []client.Object{ext, rev1}
+ },
+ validate: func(t *testing.T, c client.Client) {
+ rev := &ocv1.ClusterExtensionRevision{}
+ err := c.Get(t.Context(), client.ObjectKey{
+ Name: clusterExtensionRevisionName,
+ }, rev)
+ require.NoError(t, err)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionUnknown, cond.Status)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonReconciling, cond.Reason)
+ require.Equal(t, "some error", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
+ },
+ },
+ {
+ name: "set Available:False:RollingOut status condition during rollout when no probe failures are detected",
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ revisionResult: mockRevisionResult{},
+ existingObjs: func() []client.Object {
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
+ return []client.Object{ext, rev1}
+ },
+ validate: func(t *testing.T, c client.Client) {
+ rev := &ocv1.ClusterExtensionRevision{}
+ err := c.Get(t.Context(), client.ObjectKey{
+ Name: clusterExtensionRevisionName,
+ }, rev)
+ require.NoError(t, err)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionFalse, cond.Status)
+ require.Equal(t, ocv1.ReasonRollingOut, cond.Reason)
+ require.Equal(t, "Revision 1.0.0 is rolling out.", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
+ },
+ },
+ {
+ name: "set Available:False:ProbeFailure condition when probe failures are detected and revision is in transition",
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ revisionResult: mockRevisionResult{
+ inTransition: true,
+ isComplete: false,
+ phases: []machinery.PhaseResult{
+ mockPhaseResult{
+ name: "somephase",
+ isComplete: false,
+ objects: []machinery.ObjectResult{
+ mockObjectResult{
+ success: true,
+ probes: map[string]machinery.ObjectProbeResult{
+ boxcutter.ProgressProbeType: {
+ Success: true,
+ },
+ },
+ },
+ mockObjectResult{
+ success: false,
+ object: func() client.Object {
+ obj := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-service",
+ Namespace: "my-namespace",
+ },
+ }
+ obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service"))
+ return obj
+ }(),
+ probes: map[string]machinery.ObjectProbeResult{
+ boxcutter.ProgressProbeType: {
+ Success: false,
+ Messages: []string{
+ "something bad happened",
+ "something worse happened",
+ },
+ },
+ },
+ },
+ },
+ },
+ mockPhaseResult{
+ name: "someotherphase",
+ isComplete: false,
+ objects: []machinery.ObjectResult{
+ mockObjectResult{
+ success: false,
+ object: func() client.Object {
+ obj := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-configmap",
+ Namespace: "my-namespace",
+ },
+ }
+ obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
+ return obj
+ }(),
+ probes: map[string]machinery.ObjectProbeResult{
+ boxcutter.ProgressProbeType: {
+ Success: false,
+ Messages: []string{
+ "we have a problem",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
return []client.Object{ext, rev1}
},
validate: func(t *testing.T, c client.Client) {
@@ -81,14 +225,17 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
require.NotNil(t, cond)
require.Equal(t, metav1.ConditionFalse, cond.Status)
- require.Equal(t, ocv1.ClusterExtensionRevisionReasonIncomplete, cond.Reason)
- require.Equal(t, "Revision has not been rolled out completely.", cond.Message)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason)
+ require.Equal(t, "Object Service.v1 my-namespace/my-service: something bad happened and something worse happened\nObject ConfigMap.v1 my-namespace/my-configmap: we have a problem", cond.Message)
require.Equal(t, int64(1), cond.ObservedGeneration)
},
},
{
- name: "set Available:False:ProbeFailure condition when probe failures are detected",
+ name: "set Available:False:ProbeFailure condition when probe failures are detected and revision is not in transition",
+ reconcilingRevisionName: clusterExtensionRevisionName,
revisionResult: mockRevisionResult{
+ inTransition: false,
+ isComplete: false,
phases: []machinery.PhaseResult{
mockPhaseResult{
name: "somephase",
@@ -157,8 +304,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
return []client.Object{ext, rev1}
},
validate: func(t *testing.T, c client.Client) {
@@ -176,14 +322,37 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
},
},
{
- name: "set Progressing:True:Progressing condition while revision is transitioning",
+ name: "set Progressing:True:Retrying when there's an error reconciling the revision",
+ revisionReconcileErr: errors.New("some error"),
+ reconcilingRevisionName: clusterExtensionRevisionName,
+ existingObjs: func() []client.Object {
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
+ return []client.Object{ext, rev1}
+ },
+ validate: func(t *testing.T, c client.Client) {
+ rev := &ocv1.ClusterExtensionRevision{}
+ err := c.Get(t.Context(), client.ObjectKey{
+ Name: clusterExtensionRevisionName,
+ }, rev)
+ require.NoError(t, err)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionTrue, cond.Status)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason)
+ require.Equal(t, "some error", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
+ },
+ },
+ {
+ name: "set Progressing:True:RollingOut condition while revision is transitioning",
revisionResult: mockRevisionResult{
inTransition: true,
},
+ reconcilingRevisionName: clusterExtensionRevisionName,
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
return []client.Object{ext, rev1}
},
validate: func(t *testing.T, c client.Client) {
@@ -195,25 +364,25 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing)
require.NotNil(t, cond)
require.Equal(t, metav1.ConditionTrue, cond.Status)
- require.Equal(t, ocv1.ClusterExtensionRevisionReasonProgressing, cond.Reason)
- require.Equal(t, "Rollout in progress.", cond.Message)
+ require.Equal(t, ocv1.ReasonRollingOut, cond.Reason)
+ require.Equal(t, "Revision 1.0.0 is rolling out.", cond.Message)
require.Equal(t, int64(1), cond.ObservedGeneration)
},
},
{
- name: "remove Progressing condition once transition rollout is finished",
+ name: "set Progressing:True:Succeeded once transition rollout is finished",
revisionResult: mockRevisionResult{
inTransition: false,
},
+ reconcilingRevisionName: clusterExtensionRevisionName,
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{
Type: ocv1.TypeProgressing,
Status: metav1.ConditionTrue,
- Reason: ocv1.ClusterExtensionRevisionReasonProgressing,
- Message: "some message",
+ Reason: ocv1.ReasonSucceeded,
+ Message: "Revision 1.0.0 is rolling out.",
ObservedGeneration: 1,
})
return []client.Object{ext, rev1}
@@ -225,18 +394,22 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
}, rev)
require.NoError(t, err)
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing)
- require.Nil(t, cond)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionTrue, cond.Status)
+ require.Equal(t, ocv1.ReasonSucceeded, cond.Reason)
+ require.Equal(t, "Revision 1.0.0 has rolled out.", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
},
},
{
- name: "set Available:True:Available and Succeeded:True:RolloutSuccess conditions on successful revision rollout",
+ name: "set Available:True:ProbesSucceeded and Succeeded:True:Succeeded conditions on successful revision rollout",
revisionResult: mockRevisionResult{
isComplete: true,
},
+ reconcilingRevisionName: clusterExtensionRevisionName,
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
return []client.Object{ext, rev1}
},
validate: func(t *testing.T, c client.Client) {
@@ -248,14 +421,21 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
require.NotNil(t, cond)
require.Equal(t, metav1.ConditionTrue, cond.Status)
- require.Equal(t, ocv1.ClusterExtensionRevisionReasonAvailable, cond.Reason)
- require.Equal(t, "Object is available and passes all probes.", cond.Message)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason)
+ require.Equal(t, "Objects are available and pass all probes.", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
+
+ cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionTrue, cond.Status)
+ require.Equal(t, ocv1.ReasonSucceeded, cond.Reason)
+ require.Equal(t, "Revision 1.0.0 has rolled out.", cond.Message)
require.Equal(t, int64(1), cond.ObservedGeneration)
cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded)
require.NotNil(t, cond)
require.Equal(t, metav1.ConditionTrue, cond.Status)
- require.Equal(t, ocv1.ClusterExtensionRevisionReasonRolloutSuccess, cond.Reason)
+ require.Equal(t, ocv1.ReasonSucceeded, cond.Reason)
require.Equal(t, "Revision succeeded rolling out.", cond.Message)
require.Equal(t, int64(1), cond.ObservedGeneration)
},
@@ -265,30 +445,33 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
revisionResult: mockRevisionResult{
isComplete: true,
},
+ reconcilingRevisionName: "test-ext-3",
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- prevRev1 := newTestClusterExtensionRevision(t, "prev-rev-1")
- require.NoError(t, controllerutil.SetControllerReference(ext, prevRev1, testScheme))
- prevRev2 := newTestClusterExtensionRevision(t, "prev-rev-2")
- require.NoError(t, controllerutil.SetControllerReference(ext, prevRev2, testScheme))
- currentRev := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- currentRev.Spec.Revision = 3
- require.NoError(t, controllerutil.SetControllerReference(ext, currentRev, testScheme))
- return []client.Object{ext, prevRev1, prevRev2, currentRev}
+ prevRev1 := newTestClusterExtensionRevision(t, "test-ext-1", ext, testScheme)
+ prevRev2 := newTestClusterExtensionRevision(t, "test-ext-2", ext, testScheme)
+ rev := newTestClusterExtensionRevision(t, "test-ext-3", ext, testScheme)
+ return []client.Object{ext, prevRev1, prevRev2, rev}
},
validate: func(t *testing.T, c client.Client) {
rev := &ocv1.ClusterExtensionRevision{}
err := c.Get(t.Context(), client.ObjectKey{
- Name: "prev-rev-1",
+ Name: "test-ext-1",
}, rev)
require.NoError(t, err)
require.Equal(t, ocv1.ClusterExtensionRevisionLifecycleStateArchived, rev.Spec.LifecycleState)
err = c.Get(t.Context(), client.ObjectKey{
- Name: "prev-rev-2",
+ Name: "test-ext-2",
}, rev)
require.NoError(t, err)
require.Equal(t, ocv1.ClusterExtensionRevisionLifecycleStateArchived, rev.Spec.LifecycleState)
+
+ err = c.Get(t.Context(), client.ObjectKey{
+ Name: "test-ext-3",
+ }, rev)
+ require.NoError(t, err)
+ require.Equal(t, ocv1.ClusterExtensionRevisionLifecycleStateActive, rev.Spec.LifecycleState)
},
},
} {
@@ -305,19 +488,23 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *te
Client: testClient,
RevisionEngine: &mockRevisionEngine{
reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) {
- return tc.revisionResult, nil
+ return tc.revisionResult, tc.revisionReconcileErr
},
},
TrackingCache: &mockTrackingCache{client: testClient},
}).Reconcile(t.Context(), ctrl.Request{
NamespacedName: types.NamespacedName{
- Name: clusterExtensionRevisionName,
+ Name: tc.reconcilingRevisionName,
},
})
- // reconcile cluster extensionr evision
+ // reconcile cluster extension revision
require.Equal(t, ctrl.Result{}, result)
- require.NoError(t, err)
+ if tc.revisionReconcileErr == nil {
+ require.NoError(t, err)
+ } else {
+ require.Contains(t, err.Error(), tc.revisionReconcileErr.Error())
+ }
// validate test case
tc.validate(t, testClient)
@@ -406,8 +593,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_ValidationError_Retries(t
} {
t.Run(tc.name, func(t *testing.T) {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
// create extension and cluster extension
testClient := fake.NewClientBuilder().
@@ -431,7 +617,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_ValidationError_Retries(t
},
})
- // reconcile cluster extensionr evision
+ // reconcile cluster extension revision
require.Equal(t, ctrl.Result{
RequeueAfter: 10 * time.Second,
}, result)
@@ -454,13 +640,15 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
revisionResult machinery.RevisionResult
revisionEngineTeardownFn func(*testing.T) func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error)
validate func(*testing.T, client.Client)
+ trackingCacheFreeFn func(context.Context, client.Object) error
expectedErr string
}{
{
name: "teardown finalizer is removed",
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
@@ -483,12 +671,11 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()}
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
return []client.Object{rev1, ext}
},
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
@@ -505,20 +692,19 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
Name: clusterExtensionRevisionName,
}, rev)
require.Error(t, err)
- require.True(t, errors.IsNotFound(err))
+ require.True(t, apierrors.IsNotFound(err))
},
},
{
- name: "surfaces tear down errors when deleted",
+ name: "set Available:Unknown:Reconciling and surface tear down errors when deleted",
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()}
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
return []client.Object{rev1, ext}
},
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
@@ -535,19 +721,62 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
}, rev)
require.NoError(t, err)
require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionUnknown, cond.Status)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonReconciling, cond.Reason)
+ require.Equal(t, "some teardown error", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
},
},
{
- name: "set Available condition to Unknown with reason Archived when archiving revision",
+ name: "set Available:Unknown:Reconciling and surface tracking cache cleanup errors when deleted",
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
+ rev1.Finalizers = []string{
+ "olm.operatorframework.io/teardown",
+ }
+ rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()}
+ return []client.Object{rev1, ext}
+ },
+ revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
+ return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
+ return &mockRevisionTeardownResult{
+ isComplete: true,
+ }, nil
+ }
+ },
+ trackingCacheFreeFn: func(ctx context.Context, object client.Object) error {
+ return fmt.Errorf("some tracking cache cleanup error")
+ },
+ expectedErr: "some tracking cache cleanup error",
+ validate: func(t *testing.T, c client.Client) {
+ t.Log("cluster revision is not deleted and still contains finalizer")
+ rev := &ocv1.ClusterExtensionRevision{}
+ err := c.Get(t.Context(), client.ObjectKey{
+ Name: clusterExtensionRevisionName,
+ }, rev)
+ require.NoError(t, err)
+ cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionUnknown, cond.Status)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonReconciling, cond.Reason)
+ require.Equal(t, "some tracking cache cleanup error", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
+ },
+ },
+ {
+ name: "set Available:Archived:Unknown and Progressing:False:Archived conditions when a revision is archived",
+ revisionResult: mockRevisionResult{},
+ existingObjs: func() []client.Object {
+ ext := newTestClusterExtension()
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
return []client.Object{rev1, ext}
},
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
@@ -569,6 +798,13 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason)
require.Equal(t, "revision is archived", cond.Message)
require.Equal(t, int64(1), cond.ObservedGeneration)
+
+ cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(t, cond)
+ require.Equal(t, metav1.ConditionFalse, cond.Status)
+ require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason)
+ require.Equal(t, "revision is archived", cond.Message)
+ require.Equal(t, int64(1), cond.ObservedGeneration)
},
},
{
@@ -576,7 +812,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
@@ -588,7 +824,13 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
Message: "revision is archived",
ObservedGeneration: rev1.Generation,
})
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
+ meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{
+ Type: ocv1.ClusterExtensionRevisionTypeProgressing,
+ Status: metav1.ConditionFalse,
+ Reason: ocv1.ClusterExtensionRevisionReasonArchived,
+ Message: "revision is archived",
+ ObservedGeneration: rev1.Generation,
+ })
return []client.Object{rev1, ext}
},
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
@@ -612,12 +854,11 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
revisionResult: mockRevisionResult{},
existingObjs: func() []client.Object {
ext := newTestClusterExtension()
- rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName)
+ rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme)
rev1.Finalizers = []string{
"olm.operatorframework.io/teardown",
}
rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived
- require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme))
return []client.Object{rev1, ext}
},
revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) {
@@ -654,7 +895,10 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) {
},
teardown: tc.revisionEngineTeardownFn(t),
},
- TrackingCache: &mockTrackingCache{client: testClient},
+ TrackingCache: &mockTrackingCache{
+ client: testClient,
+ freeFn: tc.trackingCacheFreeFn,
+ },
}).Reconcile(t.Context(), ctrl.Request{
NamespacedName: types.NamespacedName{
Name: clusterExtensionRevisionName,
@@ -696,23 +940,30 @@ func newTestClusterExtension() *ocv1.ClusterExtension {
}
}
-func newTestClusterExtensionRevision(t *testing.T, name string) *ocv1.ClusterExtensionRevision {
+func newTestClusterExtensionRevision(t *testing.T, revisionName string, ext *ocv1.ClusterExtension, scheme *runtime.Scheme) *ocv1.ClusterExtensionRevision {
t.Helper()
// Extract revision number from name (e.g., "rev-1" -> 1, "test-ext-10" -> 10)
- revNum := controllers.ExtractRevisionNumber(t, name)
+ revNum := controllers.ExtractRevisionNumber(t, revisionName)
- return &ocv1.ClusterExtensionRevision{
+ rev := &ocv1.ClusterExtensionRevision{
ObjectMeta: metav1.ObjectMeta{
- Name: name,
- UID: types.UID(name),
+ Name: revisionName,
+ UID: types.UID(revisionName),
Generation: int64(1),
+ Annotations: map[string]string{
+ labels.PackageNameKey: "some-package",
+ labels.BundleNameKey: "some-package.v1.0.0",
+ labels.BundleReferenceKey: "registry.io/some-repo/some-package:v1.0.0",
+ labels.BundleVersionKey: "1.0.0",
+ },
Labels: map[string]string{
labels.OwnerNameKey: "test-ext",
},
},
Spec: ocv1.ClusterExtensionRevisionSpec{
- Revision: revNum,
+ LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive,
+ Revision: revNum,
Phases: []ocv1.ClusterExtensionRevisionPhase{
{
Name: "everything",
@@ -733,6 +984,8 @@ func newTestClusterExtensionRevision(t *testing.T, name string) *ocv1.ClusterExt
},
},
}
+ require.NoError(t, controllerutil.SetControllerReference(ext, rev, scheme))
+ return rev
}
type mockRevisionEngine struct {
@@ -883,6 +1136,7 @@ func (m mockRevisionTeardownResult) String() string {
type mockTrackingCache struct {
client client.Client
+ freeFn func(context.Context, client.Object) error
}
func (m *mockTrackingCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
@@ -902,5 +1156,8 @@ func (m *mockTrackingCache) Watch(ctx context.Context, user client.Object, gvks
}
func (m *mockTrackingCache) Free(ctx context.Context, user client.Object) error {
+ if m.freeFn != nil {
+ return m.freeFn(ctx, user)
+ }
return nil
}
diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml
index e536cd72a..862edac12 100644
--- a/manifests/experimental-e2e.yaml
+++ b/manifests/experimental-e2e.yaml
@@ -644,6 +644,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=='Available')].status
name: Available
type: string
+ - jsonPath: .status.conditions[?(@.type=='Progressing')].status
+ name: Progressing
+ type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
@@ -815,22 +818,21 @@ spec:
ClusterExtensionRevision.
The Progressing condition represents whether the revision is actively rolling out:
- - When status is True and reason is Progressing, the revision rollout is actively making progress and is in transition.
- - When Progressing is not present, the revision is not currently in transition.
+ - When status is True and reason is RollingOut, the ClusterExtensionRevision rollout is actively making progress and is in transition.
+ - When status is True and reason is Retrying, the ClusterExtensionRevision has encountered an error that could be resolved on subsequent reconciliation attempts.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has reached the desired state.
+ - When status is False and reason is Blocked, the ClusterExtensionRevision has encountered an error that requires manual intervention for recovery.
+ - When status is False and reason is Archived, the ClusterExtensionRevision is archived and not being actively reconciled.
The Available condition represents whether the revision has been successfully rolled out and is available:
- - When status is True and reason is Available, the revision has been successfully rolled out and all objects pass their readiness probes.
- - When status is False and reason is Incomplete, the revision rollout has not yet completed but no specific failures have been detected.
+ - When status is True and reason is ProbesSucceeded, the ClusterExtensionRevision has been successfully rolled out and all objects pass their readiness probes.
- When status is False and reason is ProbeFailure, one or more objects are failing their readiness probes during rollout.
- - When status is False and reason is ReconcileFailure, the revision has encountered a general reconciliation failure.
- - When status is False and reason is RevisionValidationFailure, the revision failed preflight validation checks.
- - When status is False and reason is PhaseValidationError, a phase within the revision failed preflight validation checks.
- - When status is False and reason is ObjectCollisions, objects in the revision collide with existing cluster objects that cannot be adopted.
- - When status is Unknown and reason is Archived, the revision has been archived and its objects have been torn down.
- - When status is Unknown and reason is Migrated, the revision was migrated from an existing release and object status probe results have not yet been observed.
+ - When status is Unknown and reason is Reconciling, the ClusterExtensionRevision has encountered an error that prevented it from observing the probes.
+ - When status is Unknown and reason is Archived, the ClusterExtensionRevision has been archived and its objects have been torn down.
+ - When status is Unknown and reason is Migrated, the ClusterExtensionRevision was migrated from an existing release and object status probe results have not yet been observed.
The Succeeded condition represents whether the revision has successfully completed its rollout:
- - When status is True and reason is RolloutSuccess, the revision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
@@ -981,9 +983,10 @@ spec:
inline contains JSON or YAML values specified directly in the
ClusterExtension.
- inline must be set if configType is 'Inline'.
- inline accepts arbitrary JSON/YAML objects.
- inline is validation at runtime against the schema provided by the bundle if a schema is provided.
+ inline is used to specify arbitrary configuration values for the ClusterExtension.
+ It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
+ The configuration values are validated at runtime against a JSON schema provided by the bundle.
+ minProperties: 1
type: object
x-kubernetes-preserve-unknown-fields: true
required:
@@ -1403,6 +1406,88 @@ spec:
description: status is an optional field that defines the observed state
of the ClusterExtension.
properties:
+ activeRevisions:
+ description: |-
+ activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
+ including both installed and rolling out revisions.
+ items:
+ description: RevisionStatus defines the observed state of a ClusterExtensionRevision.
+ properties:
+ conditions:
+ description: |-
+ conditions optionally expose Progressing and Available condition of the revision,
+ in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
+ Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
+ to get more insights about reasons for its current state.
+ items:
+ description: Condition contains details for one aspect of
+ the current state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ name:
+ description: name of the ClusterExtensionRevision resource
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
The set of condition types which apply to all spec.source variations are Installed and Progressing.
@@ -1416,6 +1501,8 @@ spec:
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
+ When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
+
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml
index f88debab0..3ea459499 100644
--- a/manifests/experimental.yaml
+++ b/manifests/experimental.yaml
@@ -609,6 +609,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=='Available')].status
name: Available
type: string
+ - jsonPath: .status.conditions[?(@.type=='Progressing')].status
+ name: Progressing
+ type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
@@ -780,22 +783,21 @@ spec:
ClusterExtensionRevision.
The Progressing condition represents whether the revision is actively rolling out:
- - When status is True and reason is Progressing, the revision rollout is actively making progress and is in transition.
- - When Progressing is not present, the revision is not currently in transition.
+ - When status is True and reason is RollingOut, the ClusterExtensionRevision rollout is actively making progress and is in transition.
+ - When status is True and reason is Retrying, the ClusterExtensionRevision has encountered an error that could be resolved on subsequent reconciliation attempts.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has reached the desired state.
+ - When status is False and reason is Blocked, the ClusterExtensionRevision has encountered an error that requires manual intervention for recovery.
+ - When status is False and reason is Archived, the ClusterExtensionRevision is archived and not being actively reconciled.
The Available condition represents whether the revision has been successfully rolled out and is available:
- - When status is True and reason is Available, the revision has been successfully rolled out and all objects pass their readiness probes.
- - When status is False and reason is Incomplete, the revision rollout has not yet completed but no specific failures have been detected.
+ - When status is True and reason is ProbesSucceeded, the ClusterExtensionRevision has been successfully rolled out and all objects pass their readiness probes.
- When status is False and reason is ProbeFailure, one or more objects are failing their readiness probes during rollout.
- - When status is False and reason is ReconcileFailure, the revision has encountered a general reconciliation failure.
- - When status is False and reason is RevisionValidationFailure, the revision failed preflight validation checks.
- - When status is False and reason is PhaseValidationError, a phase within the revision failed preflight validation checks.
- - When status is False and reason is ObjectCollisions, objects in the revision collide with existing cluster objects that cannot be adopted.
- - When status is Unknown and reason is Archived, the revision has been archived and its objects have been torn down.
- - When status is Unknown and reason is Migrated, the revision was migrated from an existing release and object status probe results have not yet been observed.
+ - When status is Unknown and reason is Reconciling, the ClusterExtensionRevision has encountered an error that prevented it from observing the probes.
+ - When status is Unknown and reason is Archived, the ClusterExtensionRevision has been archived and its objects have been torn down.
+ - When status is Unknown and reason is Migrated, the ClusterExtensionRevision was migrated from an existing release and object status probe results have not yet been observed.
The Succeeded condition represents whether the revision has successfully completed its rollout:
- - When status is True and reason is RolloutSuccess, the revision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
+ - When status is True and reason is Succeeded, the ClusterExtensionRevision has successfully completed its rollout. This condition is set once and persists even if the revision later becomes unavailable.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
@@ -946,9 +948,10 @@ spec:
inline contains JSON or YAML values specified directly in the
ClusterExtension.
- inline must be set if configType is 'Inline'.
- inline accepts arbitrary JSON/YAML objects.
- inline is validation at runtime against the schema provided by the bundle if a schema is provided.
+ inline is used to specify arbitrary configuration values for the ClusterExtension.
+ It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
+ The configuration values are validated at runtime against a JSON schema provided by the bundle.
+ minProperties: 1
type: object
x-kubernetes-preserve-unknown-fields: true
required:
@@ -1368,6 +1371,88 @@ spec:
description: status is an optional field that defines the observed state
of the ClusterExtension.
properties:
+ activeRevisions:
+ description: |-
+ activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
+ including both installed and rolling out revisions.
+ items:
+ description: RevisionStatus defines the observed state of a ClusterExtensionRevision.
+ properties:
+ conditions:
+ description: |-
+ conditions optionally expose Progressing and Available condition of the revision,
+ in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
+ Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
+ to get more insights about reasons for its current state.
+ items:
+ description: Condition contains details for one aspect of
+ the current state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ name:
+ description: name of the ClusterExtensionRevision resource
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
The set of condition types which apply to all spec.source variations are Installed and Progressing.
@@ -1381,6 +1466,8 @@ spec:
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
+ When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
+
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
diff --git a/openshift/operator-controller/manifests-experimental.yaml b/openshift/operator-controller/manifests-experimental.yaml
index 6ecb52ff2..42083db26 100644
--- a/openshift/operator-controller/manifests-experimental.yaml
+++ b/openshift/operator-controller/manifests-experimental.yaml
@@ -170,9 +170,10 @@ spec:
inline contains JSON or YAML values specified directly in the
ClusterExtension.
- inline must be set if configType is 'Inline'.
- inline accepts arbitrary JSON/YAML objects.
- inline is validation at runtime against the schema provided by the bundle if a schema is provided.
+ inline is used to specify arbitrary configuration values for the ClusterExtension.
+ It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
+ The configuration values are validated at runtime against a JSON schema provided by the bundle.
+ minProperties: 1
type: object
x-kubernetes-preserve-unknown-fields: true
required:
@@ -577,6 +578,86 @@ spec:
status:
description: status is an optional field that defines the observed state of the ClusterExtension.
properties:
+ activeRevisions:
+ description: |-
+ activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
+ including both installed and rolling out revisions.
+ items:
+ description: RevisionStatus defines the observed state of a ClusterExtensionRevision.
+ properties:
+ conditions:
+ description: |-
+ conditions optionally expose Progressing and Available condition of the revision,
+ in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
+ Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
+ to get more insights about reasons for its current state.
+ items:
+ description: Condition contains details for one aspect of the current state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ name:
+ description: name of the ClusterExtensionRevision resource
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
conditions:
description: |-
The set of condition types which apply to all spec.source variations are Installed and Progressing.
@@ -590,6 +671,8 @@ spec:
When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts.
When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery.
+ When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.
+
When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition.
These are indications from a package owner to guide users away from a particular package, channel, or bundle.
BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog.
diff --git a/requirements.txt b/requirements.txt
index ba058cbbc..35b0e3fc6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,5 +31,5 @@ regex==2025.11.3
requests==2.32.5
six==1.17.0
soupsieve==2.8
-urllib3==2.5.0
+urllib3==2.6.0
watchdog==6.0.0
diff --git a/test/e2e/cluster_extension_revision_test.go b/test/e2e/cluster_extension_revision_test.go
new file mode 100644
index 000000000..5bf5f2d70
--- /dev/null
+++ b/test/e2e/cluster_extension_revision_test.go
@@ -0,0 +1,261 @@
+package e2e
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "slices"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ apimeta "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/tools/remotecommand"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ ocv1 "github.com/operator-framework/operator-controller/api/v1"
+ "github.com/operator-framework/operator-controller/internal/operator-controller/features"
+ . "github.com/operator-framework/operator-controller/internal/shared/util/testutils"
+ . "github.com/operator-framework/operator-controller/test/helpers"
+)
+
+func TestClusterExtensionRevision(t *testing.T) {
+ SkipIfFeatureGateDisabled(t, string(features.BoxcutterRuntime))
+ t.Log("When a cluster extension is installed from a catalog")
+ t.Log("When the extension bundle format is registry+v1")
+
+ clusterExtension, extensionCatalog, sa, ns := TestInit(t)
+ defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns)
+ defer CollectTestArtifacts(t, artifactName, c, cfg)
+
+ clusterExtension.Spec = ocv1.ClusterExtensionSpec{
+ Source: ocv1.SourceConfig{
+ SourceType: "Catalog",
+ Catalog: &ocv1.CatalogFilter{
+ PackageName: "test",
+ Version: "1.0.1",
+ // we would also like to force upgrade to 1.0.2, which is not within the upgrade path
+ UpgradeConstraintPolicy: ocv1.UpgradeConstraintPolicySelfCertified,
+ Selector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name},
+ },
+ },
+ },
+ Namespace: ns.Name,
+ ServiceAccount: ocv1.ServiceAccountReference{
+ Name: sa.Name,
+ },
+ }
+ t.Log("It resolves the specified package with correct bundle path")
+ t.Log("By creating the ClusterExtension resource")
+ require.NoError(t, c.Create(context.Background(), clusterExtension))
+
+ t.Log("By eventually reporting a successful resolution and bundle path")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ }, pollDuration, pollInterval)
+
+ t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions")
+ var clusterExtensionRevision ocv1.ClusterExtensionRevision
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By eventually reporting progressing as True")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By eventually installing the package successfully")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+ require.Contains(ct, cond.Message, "Installed bundle")
+ require.NotEmpty(ct, clusterExtension.Status.Install.Bundle)
+ require.Len(ct, clusterExtension.Status.ActiveRevisions, 1)
+ require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, clusterExtensionRevision.Name)
+ require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions)
+ }, pollDuration, pollInterval)
+
+ t.Log("Check Deployment Availability Probe")
+ t.Log("By making the operator pod not ready")
+ podName := getPodName(t, clusterExtension.Spec.Namespace, client.MatchingLabels{"app": "olme2etest"})
+ podExec(t, clusterExtension.Spec.Namespace, podName, []string{"rm", "/var/www/ready"})
+
+ t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:False:ProbeFailure conditions")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionFalse, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By propagating Available:False to ClusterExtension")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionFalse, cond.Status)
+ }, pollDuration, pollInterval)
+
+ t.Log("By making the operator pod ready")
+ podName = getPodName(t, clusterExtension.Spec.Namespace, client.MatchingLabels{"app": "olme2etest"})
+ podExec(t, clusterExtension.Spec.Namespace, podName, []string{"touch", "/var/www/ready"})
+
+ t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By propagating Available:True to ClusterExtension")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ }, pollDuration, pollInterval)
+
+ t.Log("Check archiving")
+ t.Log("By upgrading the cluster extension to v1.2.0")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ clusterExtension.Spec.Source.Catalog.Version = "1.2.0"
+ require.NoError(t, c.Update(context.Background(), clusterExtension))
+ }, pollDuration, pollInterval)
+
+ t.Log("By revision-2 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-2", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By eventually reporting progressing, available, and installed as True")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+ require.Contains(ct, cond.Message, "Installed bundle")
+ require.NotEmpty(ct, clusterExtension.Status.Install.Bundle)
+ }, pollDuration, pollInterval)
+
+ t.Log("By revision-1 eventually reporting Progressing:False:Archived and Available:Unknown:Archived conditions")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionFalse, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionUnknown, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By upgrading the cluster extension to v1.0.2 containing bad image reference")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ clusterExtension.Spec.Source.Catalog.Version = "1.0.2"
+ require.NoError(t, c.Update(context.Background(), clusterExtension))
+ }, pollDuration, pollInterval)
+
+ t.Log("By revision-3 eventually reporting Progressing:True:Succeeded and Available:False:ProbeFailure conditions")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-3", clusterExtension.Name)}, &clusterExtensionRevision))
+ cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionTrue, cond.Status)
+ require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason)
+
+ cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable)
+ require.NotNil(ct, cond)
+ require.Equal(ct, metav1.ConditionFalse, cond.Status)
+ require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason)
+ }, pollDuration, pollInterval)
+
+ t.Log("By eventually reporting more than one active revision")
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension))
+ require.Len(ct, clusterExtension.Status.ActiveRevisions, 2)
+ require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, fmt.Sprintf("%s-2", clusterExtension.Name))
+ require.Equal(ct, clusterExtension.Status.ActiveRevisions[1].Name, fmt.Sprintf("%s-3", clusterExtension.Name))
+ require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions)
+ require.NotEmpty(ct, clusterExtension.Status.ActiveRevisions[1].Conditions)
+ }, pollDuration, pollInterval)
+}
+
+func getPodName(t *testing.T, podNamespace string, matchingLabels client.MatchingLabels) string {
+ var podList corev1.PodList
+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
+ require.NoError(ct, c.List(context.Background(), &podList, client.InNamespace(podNamespace), matchingLabels))
+ podList.Items = slices.DeleteFunc(podList.Items, func(pod corev1.Pod) bool {
+ // Ignore terminating pods
+ return pod.DeletionTimestamp != nil
+ })
+ require.Len(ct, podList.Items, 1)
+ }, pollDuration, pollInterval)
+ return podList.Items[0].Name
+}
+
+func podExec(t *testing.T, podNamespace string, podName string, cmd []string) {
+ req := cs.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec")
+ req.VersionedParams(&corev1.PodExecOptions{
+ Command: cmd,
+ Stdout: true,
+ }, scheme.ParameterCodec)
+ exec, err := remotecommand.NewSPDYExecutor(ctrl.GetConfigOrDie(), "POST", req.URL())
+ require.NoError(t, err)
+ err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{Stdout: os.Stdout})
+ require.NoError(t, err)
+}
diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go
index aa033a2f1..bb28ccdf7 100644
--- a/test/e2e/e2e_suite_test.go
+++ b/test/e2e/e2e_suite_test.go
@@ -8,6 +8,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -20,6 +21,7 @@ import (
var (
cfg *rest.Config
c client.Client
+ cs *kubernetes.Clientset
)
const (
@@ -35,6 +37,9 @@ func TestMain(m *testing.M) {
c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
utilruntime.Must(err)
+ cs, err = kubernetes.NewForConfig(cfg)
+ utilruntime.Must(err)
+
res := m.Run()
path := os.Getenv(testSummaryOutputEnvVar)
if path == "" {
diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml
new file mode 100644
index 000000000..0279603bf
--- /dev/null
+++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/bundle.configmap.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: test-configmap
+ annotations:
+ shouldNotTemplate: >
+ The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles.
+
+data:
+ version: "v1.0.2"
+ name: "test-configmap"
diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml
new file mode 100644
index 000000000..44e64cef7
--- /dev/null
+++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/olm.operatorframework.com_olme2etest.yaml
@@ -0,0 +1,27 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.1
+ name: olme2etests.olm.operatorframework.io
+spec:
+ group: olm.operatorframework.io
+ names:
+ kind: OLME2ETest
+ listKind: OLME2ETestList
+ plural: olme2etests
+ singular: olme2etest
+ scope: Cluster
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ testField:
+ type: string
diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml
new file mode 100644
index 000000000..f39fd69f5
--- /dev/null
+++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.clusterserviceversion.yaml
@@ -0,0 +1,151 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+ annotations:
+ alm-examples: |-
+ [
+ {
+ "apiVersion": "olme2etests.olm.operatorframework.io/v1",
+ "kind": "OLME2ETests",
+ "metadata": {
+ "labels": {
+ "app.kubernetes.io/managed-by": "kustomize",
+ "app.kubernetes.io/name": "test"
+ },
+ "name": "test-sample"
+ },
+ "spec": null
+ }
+ ]
+ capabilities: Basic Install
+ createdAt: "2024-10-24T19:21:40Z"
+ operators.operatorframework.io/builder: operator-sdk-v1.34.1
+ operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
+ name: testoperator.v1.0.2
+ namespace: placeholder
+spec:
+ apiservicedefinitions: {}
+ customresourcedefinitions:
+ owned:
+ - description: Configures subsections of Alertmanager configuration specific to each namespace
+ displayName: OLME2ETest
+ kind: OLME2ETest
+ name: olme2etests.olm.operatorframework.io
+ version: v1
+ description: OLM E2E Testing Operator with a wrong image ref
+ displayName: test-operator
+ icon:
+ - base64data: ""
+ mediatype: ""
+ install:
+ spec:
+ deployments:
+ - label:
+ app.kubernetes.io/component: controller
+ app.kubernetes.io/name: test-operator
+ app.kubernetes.io/version: 1.0.2
+ name: test-operator
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: olme2etest
+ template:
+ metadata:
+ labels:
+ app: olme2etest
+ spec:
+ terminationGracePeriodSeconds: 0
+ volumes:
+ - name: scripts
+ configMap:
+ name: httpd-script
+ defaultMode: 0755
+ containers:
+ - name: busybox-httpd-container
+ # This image ref is wrong and should trigger ImagePullBackOff condition
+ image: wrong/image
+ serviceAccountName: simple-bundle-manager
+ clusterPermissions:
+ - rules:
+ - apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+ - apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
+ serviceAccountName: simple-bundle-manager
+ permissions:
+ - rules:
+ - apiGroups:
+ - ""
+ resources:
+ - configmaps
+ - serviceaccounts
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - networking.k8s.io
+ resources:
+ - networkpolicies
+ verbs:
+ - get
+ - list
+ - create
+ - update
+ - delete
+ - apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+ serviceAccountName: simple-bundle-manager
+ strategy: deployment
+ installModes:
+ - supported: false
+ type: OwnNamespace
+ - supported: true
+ type: SingleNamespace
+ - supported: false
+ type: MultiNamespace
+ - supported: true
+ type: AllNamespaces
+ keywords:
+ - registry
+ links:
+ - name: simple-bundle
+ url: https://simple-bundle.domain
+ maintainers:
+ - email: main#simple-bundle.domain
+ name: Simple Bundle
+ maturity: beta
+ provider:
+ name: Simple Bundle
+ url: https://simple-bundle.domain
+ version: 1.0.2
diff --git a/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml
new file mode 100644
index 000000000..20a5ea834
--- /dev/null
+++ b/testdata/images/bundles/test-operator/v1.0.2/manifests/testoperator.networkpolicy.yaml
@@ -0,0 +1,8 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: test-operator-network-policy
+spec:
+ podSelector: {}
+ policyTypes:
+ - Ingress
diff --git a/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml
new file mode 100644
index 000000000..404f0f4a3
--- /dev/null
+++ b/testdata/images/bundles/test-operator/v1.0.2/metadata/annotations.yaml
@@ -0,0 +1,10 @@
+annotations:
+ # Core bundle annotations.
+ operators.operatorframework.io.bundle.mediatype.v1: registry+v1
+ operators.operatorframework.io.bundle.manifests.v1: manifests/
+ operators.operatorframework.io.bundle.metadata.v1: metadata/
+ operators.operatorframework.io.bundle.package.v1: test
+ operators.operatorframework.io.bundle.channels.v1: beta
+ operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0
+ operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
+ operators.operatorframework.io.metrics.project_layout: unknown
diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml
index 437175a4e..111c75f42 100644
--- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml
+++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml
@@ -7,6 +7,7 @@ name: alpha
package: test
entries:
- name: test-operator.1.0.0
+ - name: test-operator.1.0.2
---
schema: olm.channel
name: beta
@@ -38,6 +39,17 @@ properties:
packageName: test
version: 1.0.1
---
+# Bundle with a wrong image ref causing image pull failure
+schema: olm.bundle
+name: test-operator.1.0.2
+package: test
+image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.2
+properties:
+ - type: olm.package
+ value:
+ packageName: test
+ version: 1.0.2
+---
schema: olm.bundle
name: test-operator.1.2.0
package: test