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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .bingo/Variables.mk
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ $(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod
@echo "(re)installing $(GOBIN)/golangci-lint-v2.8.0"
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v2.8.0 "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"

KIND := $(GOBIN)/kind-v0.31.0
KIND := $(GOBIN)/kind-v0.33.0
$(KIND): $(BINGO_DIR)/kind.mod
@# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies.
@echo "(re)installing $(GOBIN)/kind-v0.31.0"
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.31.0 "sigs.k8s.io/kind"

@echo "(re)installing $(GOBIN)/kind-v0.33.0"
@cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.33.0 "sigs.k8s.io/kind"
2 changes: 1 addition & 1 deletion .bingo/kind.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT

go 1.26.5

require sigs.k8s.io/kind v0.31.0
require sigs.k8s.io/kind v0.33.0
15 changes: 2 additions & 13 deletions .bingo/kind.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,14 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand All @@ -25,11 +18,7 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/kind v0.29.0 h1:3TpCsyh908IkXXpcSnsMjWdwdWjIl7o9IMZImZCWFnI=
sigs.k8s.io/kind v0.29.0/go.mod h1:ldWQisw2NYyM6k64o/tkZng/1qQW7OlzcN5a8geJX3o=
sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g=
sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8=
sigs.k8s.io/kind v0.33.0 h1:AjvDv3vOygb/VKLVQW87lfktIBzkxR8Ump9DjxC8+Lk=
sigs.k8s.io/kind v0.33.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
3 changes: 1 addition & 2 deletions .bingo/variables.env
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ GO_APIDIFF="${GOBIN}/go-apidiff-v0.8.3"

GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.8.0"

KIND="${GOBIN}/kind-v0.31.0"

KIND="${GOBIN}/kind-v0.33.0"
17 changes: 17 additions & 0 deletions .github/workflows/migration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ jobs:
path: artifacts/e2e
if-no-files-found: ignore

in-cluster-job:
name: migration in-cluster Job E2E
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Run in-cluster migration Job test
run: make migration/test-e2e-in-cluster-job
- name: Tear down fixture cluster
if: always()
run: E2E_CLUSTER_NAME=library-olm-fixture-e2e make migration/e2e-teardown

coverage:
name: migration total coverage
needs: [unit-coverage, fixture, live]
Expand Down
15 changes: 15 additions & 0 deletions migration.mk
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ E2E_REAL_OPERATOR_MANIFEST ?= $(ROOT_DIR)/test/e2e/migration/real-operator.yaml
E2E_REAL_OPERATOR_NAMESPACE ?= migration-e2e-real
E2E_REAL_OPERATOR_SUBSCRIPTION ?= ecr-secret-operator
E2E_OPERATOR ?= all
E2E_MIGRATION_IMAGE ?= library-olm-migration-e2e:dev

##@ Migration

Expand All @@ -45,6 +46,14 @@ migration/build-catalogs: ## Build migrate-catalogs-v0-to-v1 into bin/
@mkdir -p $(BIN_DIR)
go build -cover -covermode=count -o $(MIGRATE_CATALOGS_BIN) ./migration/examples/cmd/migrate-catalogs-v0-to-v1

.PHONY: migration/build-e2e-image
migration/build-e2e-image: $(KIND) ## Build and load an uninstrumented migration CLI image into the fixture kind cluster
@mkdir -p $(BIN_DIR)
CGO_ENABLED=0 go build -o $(MIGRATE_OPERATORS_BIN) ./migration/examples/cmd/migrate-operators-v0-to-v1
CGO_ENABLED=0 go build -o $(MIGRATE_CATALOGS_BIN) ./migration/examples/cmd/migrate-catalogs-v0-to-v1
docker build --tag "$(E2E_MIGRATION_IMAGE)" --file test/e2e/migration/migration-tool.Dockerfile .
$(KIND) load docker-image --name "$(E2E_FIXTURE_CLUSTER_NAME)" "$(E2E_MIGRATION_IMAGE)"

.PHONY: migration/test-unit
migration/test-unit: ## Run migration unit tests and write a coverage profile
@mkdir -p $(COVERAGE_DIR)
Expand Down Expand Up @@ -89,6 +98,12 @@ migration/test-e2e-live-matrix: migration/build ## Install and migrate all three
migration/test-e2e-fixture-matrix: migration/build ## Replay and migrate all three OLMv0 install snapshots
@set -euo pipefail; while IFS=$$'\t' read -r package channel namespace; do [[ -z "$$package" || "$$package" == \#* ]] && continue; coverage_dir="$(E2E_COVERAGE_DIR)/fixture/$$package"; artifact_dir="$(E2E_ARTIFACTS)/fixture/$$package"; mkdir -p "$$coverage_dir"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/install-fixture-v0.sh "$$package"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" GOCOVERDIR="$$coverage_dir" E2E_ARTIFACTS="$$artifact_dir" E2E_SUITE=fixture E2E_NAMESPACE="$$namespace" E2E_SUBSCRIPTION="$$package" go test -count=1 -tags=e2e ./test/e2e/migration -timeout "$(E2E_TIMEOUT)"; KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" ./hack/e2e/migration/delete-v1.sh "$$package"; done < test/e2e/migration/operators.tsv

.PHONY: migration/test-e2e-in-cluster-job
migration/test-e2e-in-cluster-job: migration/e2e-fixture-setup migration/build-e2e-image ## Run migration CLIs in a fixture-cluster Job (no coverage collection)
E2E_OPERATOR=ecr-secret-operator E2E_KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" $(MAKE) migration/e2e-delete-v1
E2E_OPERATOR=ecr-secret-operator $(MAKE) migration/e2e-install-fixture-v0
KUBECONFIG="$(E2E_FIXTURE_KUBECONFIG)" E2E_SUITE=in-cluster-job E2E_NAMESPACE=migration-e2e-ecr-secret E2E_SUBSCRIPTION=ecr-secret-operator E2E_MIGRATION_IMAGE="$(E2E_MIGRATION_IMAGE)" E2E_ARTIFACTS="$(E2E_ARTIFACTS)/in-cluster-job" go test -count=1 -tags=e2e ./test/e2e/migration -run '^TestMigrationInClusterJob$$' -timeout "$(E2E_TIMEOUT)"

.PHONY: migration/e2e-delete-v1
migration/e2e-delete-v1: ## Delete one migration E2E operator as OLMv1, or all
@if [[ "$(E2E_OPERATOR)" != all ]]; then E2E_OPERATOR="$(E2E_OPERATOR)" KUBECONFIG="$(E2E_KUBECONFIG)" bash -c 'source "$$1"; operator_fields "$$E2E_OPERATOR"' -- ./hack/e2e/migration/operators.sh; fi
Expand Down
20 changes: 19 additions & 1 deletion migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -86,7 +87,15 @@ func newClient() (client.Client, error) {

restConfig, err := kubeConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to get REST config: %w", err)
// A migration Job has no kubeconfig file. Use projected ServiceAccount
// credentials only when neither --kubeconfig nor KUBECONFIG was supplied;
// an invalid explicit configuration must retain its original error.
if shouldUseInClusterConfig(kubeconfig, kubeconfigEnvIsSet()) {
restConfig, err = rest.InClusterConfig()
}
if err != nil {
return nil, fmt.Errorf("failed to get REST config: %w", err)
}
}

c, err := client.New(restConfig, client.Options{Scheme: scheme})
Expand All @@ -96,6 +105,15 @@ func newClient() (client.Client, error) {
return c, nil
}

func kubeconfigEnvIsSet() bool {
_, set := os.LookupEnv("KUBECONFIG")
return set
}

func shouldUseInClusterConfig(kubeconfig string, kubeconfigEnvSet bool) bool {
return kubeconfig == "" && !kubeconfigEnvSet
}

func runMigrateCatalogs(cmd *cobra.Command, _ []string) error {
c, err := newClient()
if err != nil {
Expand Down
24 changes: 24 additions & 0 deletions migration/examples/cmd/migrate-catalogs-v0-to-v1/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import "testing"

func TestShouldUseInClusterConfig(t *testing.T) {
testCases := []struct {
name string
kubeconfig string
kubeconfigEnvSet bool
want bool
}{
{name: "no explicit configuration", want: true},
{name: "flag configuration", kubeconfig: "/tmp/config", want: false},
{name: "KUBECONFIG configuration", kubeconfigEnvSet: true, want: false},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
if got := shouldUseInClusterConfig(testCase.kubeconfig, testCase.kubeconfigEnvSet); got != testCase.want {
t.Fatalf("shouldUseInClusterConfig(%q, %t) = %t, want %t", testCase.kubeconfig, testCase.kubeconfigEnvSet, got, testCase.want)
}
})
}
}
19 changes: 18 additions & 1 deletion migration/examples/cmd/migrate-operators-v0-to-v1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ func newClient() (client.Client, *rest.Config, error) {

restConfig, err := kubeConfig.ClientConfig()
if err != nil {
return nil, nil, fmt.Errorf("failed to get REST config: %w", err)
// A migration Job has no kubeconfig file. Use projected ServiceAccount
// credentials only when neither --kubeconfig nor KUBECONFIG was supplied;
// an invalid explicit configuration must retain its original error.
if shouldUseInClusterConfig(kubeconfig, kubeconfigEnvIsSet()) {
restConfig, err = rest.InClusterConfig()
Comment thread
tmshort marked this conversation as resolved.
}
if err != nil {
return nil, nil, fmt.Errorf("failed to get REST config: %w", err)
}
}

c, err := client.New(restConfig, client.Options{Scheme: scheme})
Expand All @@ -88,3 +96,12 @@ func newClient() (client.Client, *rest.Config, error) {
}
return c, restConfig, nil
}

func kubeconfigEnvIsSet() bool {
_, set := os.LookupEnv("KUBECONFIG")
return set
}

func shouldUseInClusterConfig(kubeconfig string, kubeconfigEnvSet bool) bool {
return kubeconfig == "" && !kubeconfigEnvSet
}
24 changes: 24 additions & 0 deletions migration/examples/cmd/migrate-operators-v0-to-v1/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import "testing"

func TestShouldUseInClusterConfig(t *testing.T) {
testCases := []struct {
name string
kubeconfig string
kubeconfigEnvSet bool
want bool
}{
{name: "no explicit configuration", want: true},
{name: "flag configuration", kubeconfig: "/tmp/config", want: false},
{name: "KUBECONFIG configuration", kubeconfigEnvSet: true, want: false},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
if got := shouldUseInClusterConfig(testCase.kubeconfig, testCase.kubeconfigEnvSet); got != testCase.want {
t.Fatalf("shouldUseInClusterConfig(%q, %t) = %t, want %t", testCase.kubeconfig, testCase.kubeconfigEnvSet, got, testCase.want)
}
})
}
}
36 changes: 26 additions & 10 deletions specs/20260821-migration-v0-to-v1/e2e.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
# E2E Test Strategy — OLMv0 → OLMv1 Migration

This document implements the Phase 8 E2E plan from [validation.md](validation.md).
It deliberately uses two suites: deterministic fixture scenarios for the migration tool's
decision and recovery paths, and real-operator smoke scenarios for controller adoption.
Both suites are required CI gates for pull requests, merge queues, and pushes to `main`.
It uses deterministic fixture scenarios for the migration tool's decision and recovery paths,
real-operator smoke scenarios for controller adoption, and an in-cluster Job that verifies
ServiceAccount authentication. All are required CI gates for pull requests, merge queues, and
pushes to `main`.

## Test environments

| Suite | Cluster | Catalog content | Purpose |
|---|---|---|---|
| `fixture` | kind + OLMv1 + OLMv0 CRDs only (no OLMv0 controllers) | Committed OLMv0-install snapshots and a digest-pinned CatalogSource | Deterministic coverage of V1, V2, V3, V4. |
| `real-operator` | separate kind cluster with OLMv0 + OLMv1 | OLMv0's installed OperatorHub catalog | Proves V5.2–V5.8 with real deployed operators. |
| `in-cluster-job` | fixture cluster | A replayed ecr-secret-operator installation | Proves both CLIs authenticate and migrate using only a Pod ServiceAccount. |
| `kind-only` | kind + OLMv1 | Local fixture objects, no OLMv0 controllers | Fast contract tests for resource rendering and COS adoption prerequisites. |

Do not use a mutable `latest` image or an unpinned release installer in CI. The default
bootstrap pins OLMv0 to `v0.46.0`, OLMv1 to `v1.11.0`, and kind to `v0.31.0` with the
bootstrap pins OLMv0 to `v0.46.0`, OLMv1 to `v1.11.0`, and kind to `v0.33.0` with the
digest-pinned Kubernetes `v1.36.1` node image; CI should
mirror those release artifacts and override the URLs when it cannot access GitHub. The job inputs
are the kind node image, OLMv0 manifest URL and digest, operator-controller release
Expand Down Expand Up @@ -107,12 +109,13 @@ coverage while still showing which migration paths the E2E suite executes.

## CI rollout

1. The `migration-test` workflow runs unit coverage, fixture E2E, and live-operator E2E
independently, each with its own Kind cluster and kubeconfig; a fourth job merges their
coverage profiles and displays the total report.
2. Run all four jobs for pull requests, merge queues, and pushes to `main`. The fixture and
live-operator suites are required merge gates. Preserve E2E diagnostics and CLI coverage
artifacts for failed jobs.
1. The `migration-test` workflow runs unit coverage, fixture E2E, live-operator E2E, and the
in-cluster Job E2E independently. The first three provide CLI coverage; a fifth job merges
their coverage profiles and displays the total report.
2. Run all five jobs for pull requests, merge queues, and pushes to `main`. The fixture,
live-operator, and in-cluster Job suites are required merge gates. Preserve diagnostics and
CLI coverage artifacts for the coverage-producing fixture and live-operator suites; the
focused in-cluster Job check uploads neither.
3. Retry only provisioning/image-pull failures once; never retry a failed assertion automatically.

## Run order
Expand Down Expand Up @@ -173,3 +176,16 @@ For a single package, replace `all` with its name from `e2e/migration/operators.
`make migration/e2e-install-v0 E2E_OPERATOR=redis-operator`. Snapshot capture must occur while the
operator remains OLMv0-managed. Fixture CI never captures snapshots, so it can run
independently and in parallel with the live suite.

### In-cluster Job test

This focused test is independent of coverage collection. It provisions the fixture cluster,
loads a locally built, uninstrumented image into Kind, then runs catalog and operator migration
from a Job using projected ServiceAccount credentials (with a test-only `cluster-admin` binding).
It intentionally retains the Job namespace for log inspection; the next invocation removes it
before creating a fresh Job.

```bash
make migration/test-e2e-in-cluster-job
E2E_CLUSTER_NAME=library-olm-fixture-e2e make migration/e2e-teardown
```
Loading
Loading