Skip to content
Open
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
83 changes: 83 additions & 0 deletions .github/workflows/migration-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: migration-test

on:
workflow_dispatch:
pull_request:
merge_group:
push:
branches:
- main

permissions:
contents: read

jobs:
unit-coverage:
name: migration unit coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Run unit tests with coverage
run: make migration/test-coverage
- name: Upload unit coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: migration-unit-coverage
path: artifacts/coverage
if-no-files-found: ignore

coverage:
name: migration total coverage
needs: [unit-coverage, live]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Download unit coverage
uses: actions/download-artifact@v8
with:
name: migration-unit-coverage
path: artifacts/coverage
- name: Download live E2E coverage
uses: actions/download-artifact@v8
with:
name: migration-live-e2e-artifacts
path: artifacts/e2e
- name: Display combined coverage
run: make migration/report-coverage-all

live:
name: migration live-operator E2E
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Set up live cluster
run: make migration/e2e-setup
- name: Run live-operator migration tests
run: make migration/test-e2e-live-matrix
- name: Tear down live cluster
if: always()
run: make migration/e2e-teardown
- name: Upload live diagnostics and coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: migration-live-e2e-artifacts
path: artifacts/e2e
if-no-files-found: ignore
20 changes: 0 additions & 20 deletions .github/workflows/unit-test.yaml

This file was deleted.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ go.work.sum

# Generated test coverage profiles and reports.
artifacts/

# Kind-generated kubeconfigs contain cluster credentials.
.kubeconfig/

# Locally built E2E CLIs.
bin/
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ require (
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand All @@ -60,6 +62,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af // indirect
k8s.io/streaming v0.36.4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAw
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
Expand Down Expand Up @@ -73,6 +75,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand All @@ -81,6 +85,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down Expand Up @@ -192,6 +198,8 @@ k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af h1:zLXA2Irn14q2/06WMkxViyr7YCPUO2lJ0QYE9Juy5vA=
k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
k8s.io/streaming v0.36.4 h1:RS5YlhrdBN2pKGVjgygGntdu6SNdsduyjGWGe3cX0vo=
k8s.io/streaming v0.36.4/go.mod h1:tJ6S2bZa2HxIBauguBbCWSCYyd93Grfz1+z3tcOvlDE=
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
Expand Down
16 changes: 16 additions & 0 deletions hack/e2e/migration/delete-v1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/operators.sh"

operator_fields "${1:?usage: delete-v1.sh <operator>}"
# Delete CE first, then any revisions it owned. This is the explicit OLMv1
# cleanup path; it never deletes shared CRDs or the source CatalogSource.
kubectl delete "clusterextension/$E2E_PACKAGE" --ignore-not-found --wait=true
kubectl delete clusterobjectsets -l "olm.operatorframework.io/owner-name=$E2E_PACKAGE" --ignore-not-found --wait=true
# Ref Secrets live in the OLMv1 system namespace rather than the operator's
# installation namespace and are not garbage-collected by ClusterObjectSet.
# Remove them as part of test cleanup so a repeat migration can reuse its
# deterministic revision and Secret names.
kubectl -n "${E2E_OLMV1_NAMESPACE:-olmv1-system}" delete secret \
-l "olm.operatorframework.io/owner-name=$E2E_PACKAGE" \
--ignore-not-found --wait=true
36 changes: 36 additions & 0 deletions hack/e2e/migration/install-v0.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/operators.sh"

operator_fields "${1:?usage: install-v0.sh <operator>}"
kubectl delete namespace "$E2E_NAMESPACE" --ignore-not-found --wait=true
kubectl create namespace "$E2E_NAMESPACE" --dry-run=client -o yaml | kubectl apply --validate=false -f -

# Some bundles require user-supplied configuration before their controller pod
# can be created. These source-controlled inputs are also applied by fixture
# replay, keeping both suites faithful to the same installed operator.
preinstall_fixture="$root_dir/test/e2e/migration/fixtures/preinstall/$E2E_PACKAGE.yaml"
if [[ -f $preinstall_fixture ]]; then
kubectl -n "$E2E_NAMESPACE" apply --validate=false -f "$preinstall_fixture"
fi

kubectl -n "$E2E_NAMESPACE" apply --validate=false -f - <<EOF
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: migration-e2e
spec: {}
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: $E2E_PACKAGE
spec:
channel: $E2E_CHANNEL
name: $E2E_PACKAGE
source: operatorhubio-catalog
sourceNamespace: olm
EOF
kubectl -n "$E2E_NAMESPACE" wait --for=jsonpath='{.status.state}'=AtLatestKnown "subscription/$E2E_PACKAGE" --timeout=15m
csv=$(kubectl -n "$E2E_NAMESPACE" get "subscription/$E2E_PACKAGE" -o jsonpath='{.status.installedCSV}')
kubectl -n "$E2E_NAMESPACE" wait --for=jsonpath='{.status.phase}'=Succeeded "csv/$csv" --timeout=15m
26 changes: 26 additions & 0 deletions hack/e2e/migration/operators.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
operators_file="$root_dir/test/e2e/migration/operators.tsv"
: "${KUBECONFIG:?KUBECONFIG must point to the Kind-generated E2E kubeconfig}"

operator_row() {
awk -F '\t' -v name="$1" '$1 == name { print; exit }' "$operators_file"
}

operator_names() {
awk -F '\t' 'NF == 3 && $1 !~ /^#/ { print $1 }' "$operators_file"
}

for_each_operator() {
if [[ ${1:-} == all ]]; then operator_names; else printf '%s\n' "$1"; fi
}

operator_fields() {
local row
row=$(operator_row "$1")
[[ -n "$row" ]] || { echo "unknown E2E operator: $1" >&2; exit 2; }
IFS=$'\t' read -r E2E_PACKAGE E2E_CHANNEL E2E_NAMESPACE <<<"$row"
export E2E_PACKAGE E2E_CHANNEL E2E_NAMESPACE
}
50 changes: 50 additions & 0 deletions hack/e2e/migration/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail

# Creates a disposable cluster for the migration E2E suites. Manifest locations are
# required inputs: CI supplies immutable, mirrored copies when it cannot access
# the public release URLs.
: "${E2E_KUBECONFIG:?E2E_KUBECONFIG is required}"
: "${OLM_V0_CRDS:?OLM_V0_CRDS is required}"
: "${OLM_V0_MANIFEST:?OLM_V0_MANIFEST is required}"
: "${OLM_V1_INSTALL:?OLM_V1_INSTALL is required}"
: "${OLM_V1_INSTALL_SHA256:?OLM_V1_INSTALL_SHA256 is required}"
: "${KIND:?KIND is required}"

root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
cluster_name=${E2E_CLUSTER_NAME:-library-olm-e2e}
kind_config=${E2E_KIND_CONFIG:-"$root_dir/test/e2e/migration/kind-config.yaml"}

mkdir -p "$(dirname "$E2E_KUBECONFIG")"
if "$KIND" get clusters | grep -Fxq "$cluster_name"; then
# Keep an existing dedicated cluster for iterative fixture development, but
# always regenerate the kubeconfig from Kind rather than accepting an old or
# user-supplied file.
"$KIND" export kubeconfig --name "$cluster_name" --kubeconfig "$E2E_KUBECONFIG"
else
"$KIND" create cluster --name "$cluster_name" --config "$kind_config" --kubeconfig "$E2E_KUBECONFIG"
fi
kubectl --kubeconfig "$E2E_KUBECONFIG" wait --for=condition=Ready nodes --all --timeout=3m

# The v0.46 CRD bundle exceeds Kubernetes' client-side last-applied annotation
# limit for the CSV CRD. Server-side apply stores managed fields instead.
kubectl --kubeconfig "$E2E_KUBECONFIG" apply --server-side -f "$OLM_V0_CRDS"
if [[ ${E2E_INSTALL_OLMV0:-true} == true ]]; then
kubectl --kubeconfig "$E2E_KUBECONFIG" apply -f "$OLM_V0_MANIFEST"
kubectl --kubeconfig "$E2E_KUBECONFIG" -n olm wait --for=condition=Available deployment/olm-operator --timeout=5m
kubectl --kubeconfig "$E2E_KUBECONFIG" -n olm wait --for=condition=Available deployment/catalog-operator --timeout=5m
fi

# ClusterObjectSet is an experimental OLMv1 API and is required by the migration
# code, so use operator-controller's experimental installer rather than standard.
# Download a pinned release installer before executing it, restricting redirects
# to HTTPS and verifying its recorded SHA-256.
if ! kubectl --kubeconfig "$E2E_KUBECONFIG" get crd/clusterextensions.olm.operatorframework.io >/dev/null 2>&1; then
installer=$(mktemp)
trap 'rm -f "$installer"' EXIT
curl --fail --location --proto '=https' --proto-redir '=https' --silent --show-error "$OLM_V1_INSTALL" --output "$installer"
printf '%s %s\n' "$OLM_V1_INSTALL_SHA256" "$installer" | sha256sum --check --status
KUBECONFIG="$E2E_KUBECONFIG" bash "$installer"
fi
kubectl --kubeconfig "$E2E_KUBECONFIG" wait --for=condition=Established crd/clusterextensions.olm.operatorframework.io crd/clustercatalogs.olm.operatorframework.io --timeout=5m
kubectl --kubeconfig "$E2E_KUBECONFIG" -n olmv1-system wait --for=condition=Available deployment/operator-controller-controller-manager deployment/catalogd-controller-manager --timeout=5m
4 changes: 4 additions & 0 deletions hack/e2e/migration/teardown.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
: "${KIND:?KIND is required}"
"$KIND" delete cluster --name "${E2E_CLUSTER_NAME:-library-olm-e2e}"
Loading