From 805a2c1008c9f4a1eefe24842f9c5cc958928c64 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 21 Aug 2026 15:13:15 -0400 Subject: [PATCH 01/22] =?UTF-8?q?Implement=20OLMv0=E2=86=92OLMv1=20migrati?= =?UTF-8?q?on=20library=20and=20CLIs=20(Phases=201=E2=80=935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports and adapts the perdasilva/operator-controller migration prototype into a standalone library under github.com/operator-framework/library-olm. Library (migration/pkg/migration/): - Four-state scan: Eligible / Ineligible / AlreadyMigrated / Conflict - Readiness checks: Subscription state, CSV health, olm.generated-by (C9), package uniqueness - Compatibility checks: C1 AllNamespaces, C2 dependencies, C3 APIServices, C4 OperatorCondition - Resource collector: 5 strategies (Operator CR refs, CRD labels, olm.owner labels, ownerRefs, InstallPlan steps) with dedup - ClusterCatalog resolution via catalog content API proxy - PhaseSort grouping objects into ordered phases (namespaces→crds→rbac→deploy) - Full migration flow: backup → delete Sub/CSV → collect → create COS (waits Succeeded=True) → create CE → cleanup OLMv0 resources - Rollback and cleanup for recovery Key adaptations over the prototype (per REQUIREMENTS.md): - ClusterExtensionRevision → ClusterObjectSet (R2.2) - CollisionProtection: IfNoController on all COS objects (R2.4) - migrated-from-subscription annotation on both COS and CE (R2.5) - CE spec.serviceAccount not set — deprecated in OLMv1 (R2.5/R7) - No internal operator-controller package imports; constants defined locally Catalog migration (migration/pkg/catalogmigration/): - CatalogSource → ClusterCatalog with same-name deduplication strategy (R8) - Adopt existing ClusterCatalog by image match; idempotent annotation - Poll interval conversion; int32 priority validation with overflow flag - --delete-catalogsource with Subscription-reference guard CLIs (migration/examples/cmd/): - migrate-operators-v0-to-v1: check / convert / rollback / cleanup verbs - migrate-catalogs-v0-to-v1: --dry-run / --delete-catalogsource / --acknowledge-priority-overflow Phase 6 (namespace change) blocked on operator-controller PR #2825. Phase 7 (APIService renderer) is cross-repo work in operator-controller. Phase 8 (unit + E2E tests) not yet started. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- go.mod | 65 ++ go.sum | 206 ++++++ .../cmd/migrate-catalogs-v0-to-v1/main.go | 184 +++++ .../cmd/migrate-operators-v0-to-v1/check.go | 122 ++++ .../cmd/migrate-operators-v0-to-v1/cleanup.go | 111 +++ .../cmd/migrate-operators-v0-to-v1/convert.go | 306 ++++++++ .../cmd/migrate-operators-v0-to-v1/main.go | 90 +++ .../cmd/migrate-operators-v0-to-v1/output.go | 95 +++ .../migrate-operators-v0-to-v1/rollback.go | 110 +++ .../pkg/catalogmigration/catalogmigration.go | 384 ++++++++++ migration/pkg/migration/catalog.go | 283 +++++++ migration/pkg/migration/checks.go | 34 + migration/pkg/migration/collector.go | 433 +++++++++++ migration/pkg/migration/compatibility.go | 254 +++++++ migration/pkg/migration/labels.go | 36 + migration/pkg/migration/migration.go | 689 ++++++++++++++++++ migration/pkg/migration/phase.go | 195 +++++ migration/pkg/migration/readiness.go | 134 ++++ migration/pkg/migration/scan.go | 396 ++++++++++ migration/pkg/migration/types.go | 79 ++ 20 files changed, 4206 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/check.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/convert.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/main.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/output.go create mode 100644 migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go create mode 100644 migration/pkg/catalogmigration/catalogmigration.go create mode 100644 migration/pkg/migration/catalog.go create mode 100644 migration/pkg/migration/checks.go create mode 100644 migration/pkg/migration/collector.go create mode 100644 migration/pkg/migration/compatibility.go create mode 100644 migration/pkg/migration/labels.go create mode 100644 migration/pkg/migration/migration.go create mode 100644 migration/pkg/migration/phase.go create mode 100644 migration/pkg/migration/readiness.go create mode 100644 migration/pkg/migration/scan.go create mode 100644 migration/pkg/migration/types.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d6dfcb --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module github.com/operator-framework/library-olm + +go 1.26.3 + +require ( + github.com/operator-framework/api v0.45.0 + github.com/operator-framework/operator-controller v1.11.0 + github.com/spf13/cobra v1.10.2 + k8s.io/api v0.36.2 + k8s.io/apiextensions-apiserver v0.36.2 + k8s.io/apimachinery v0.36.2 + k8s.io/client-go v0.36.2 + sigs.k8s.io/controller-runtime v0.24.1 +) + +require ( + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/jsonreference v0.21.6 // indirect + github.com/go-openapi/swag v0.26.0 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + 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/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/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 + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + 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/utils v0.0.0-20260626114624-be93311217bd // 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 + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2d2f66 --- /dev/null +++ b/go.sum @@ -0,0 +1,206 @@ +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +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/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= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= +github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= +github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= +github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= +github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/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= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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/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= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y= +github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/operator-framework/api v0.45.0 h1:hkROwtsLH3oszp4IW+WsXEFSDgveSahHI7DKStOtrUI= +github.com/operator-framework/api v0.45.0/go.mod h1:IQ4uuISTiIhV09oAurJSGD4KabayhY5nV6k1XmA235M= +github.com/operator-framework/operator-controller v1.11.0 h1:I1isTdEJ5mT5WHV+o3DPI3bI+eO0xls4gaVbx8kS/Mg= +github.com/operator-framework/operator-controller v1.11.0/go.mod h1:/BvZx/whTj5qkrO6TVg9oq6C3gmmTvB6QgFGlaROfT4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +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/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4= +k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E= +k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w= +k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA= +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/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= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go b/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go new file mode 100644 index 0000000..b2053ba --- /dev/null +++ b/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go @@ -0,0 +1,184 @@ +// migrate-catalogs-v0-to-v1 migrates OLMv0 CatalogSources to OLMv1 ClusterCatalogs. +// Run this before migrate-operators-v0-to-v1. +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" + + "github.com/operator-framework/library-olm/migration/pkg/catalogmigration" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(ocv1.AddToScheme(scheme)) + utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) +} + +var ( + kubeconfig string + dryRun bool + deleteCatalogSource bool + acknowledgePriorityOverflow bool +) + +var rootCmd = &cobra.Command{ + Use: "migrate-catalogs-v0-to-v1", + Short: "Migrate OLMv0 CatalogSources to OLMv1 ClusterCatalogs", + Long: `Scans all CatalogSources across all namespaces and creates +corresponding OLMv1 ClusterCatalogs. + +Only grpc-type CatalogSources with a spec.image are migratable. +configmap, internal, and address-only CatalogSources are reported as not migratable. + +Run this before migrate-operators-v0-to-v1. + +Examples: + migrate-catalogs-v0-to-v1 + migrate-catalogs-v0-to-v1 --dry-run + migrate-catalogs-v0-to-v1 --delete-catalogsource`, + RunE: runMigrateCatalogs, +} + +func init() { + rootCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file") + rootCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Print what would be created without modifying the cluster") + rootCmd.Flags().BoolVar(&deleteCatalogSource, "delete-catalogsource", false, "Delete source CatalogSource after migration (only when no Subscription references it)") + rootCmd.Flags().BoolVar(&acknowledgePriorityOverflow, "acknowledge-priority-overflow", false, "Cap out-of-range priority at MaxInt32/MinInt32 and proceed") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func newClient() (client.Client, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfig != "" { + loadingRules.ExplicitPath = kubeconfig + } + + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}, + ) + + restConfig, err := kubeConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to get REST config: %w", err) + } + + c, err := client.New(restConfig, client.Options{Scheme: scheme}) + if err != nil { + return nil, fmt.Errorf("failed to create client: %w", err) + } + return c, nil +} + +func runMigrateCatalogs(cmd *cobra.Command, _ []string) error { + c, err := newClient() + if err != nil { + return err + } + + cm := catalogmigration.NewCatalogMigrator(c) + opts := catalogmigration.CatalogMigratorOptions{ + DryRun: dryRun, + DeleteCatalogSource: deleteCatalogSource, + AcknowledgePriorityOverflow: acknowledgePriorityOverflow, + } + + if dryRun { + fmt.Printf("\n🔍 Dry run — no cluster changes will be made.\n\n") + } else { + fmt.Printf("\n🔄 Migrating CatalogSources to ClusterCatalogs...\n\n") + } + + results, err := cm.MigrateCatalogs(cmd.Context(), opts) + if err != nil { + return fmt.Errorf("catalog migration failed: %w", err) + } + + // Print results grouped by status + var created, adopted, skipped, errored, dryResults []catalogmigration.CatalogMigrationResult + for _, r := range results { + switch r.Status { + case "created": + created = append(created, r) + case "adopted": + adopted = append(adopted, r) + case "skipped": + skipped = append(skipped, r) + case "error": + errored = append(errored, r) + case "dry-run": + dryResults = append(dryResults, r) + } + } + + if len(dryResults) > 0 { + fmt.Printf("Would migrate:\n") + for _, r := range dryResults { + fmt.Printf(" %-40s → %s\n %s\n", + r.CatalogSourceNamespace+"/"+r.CatalogSourceName, + r.ClusterCatalogName, r.Reason) + } + } + + if len(created) > 0 { + fmt.Printf("\n✅ Created (%d):\n", len(created)) + for _, r := range created { + fmt.Printf(" ✓ %s/%s → ClusterCatalog/%s\n", + r.CatalogSourceNamespace, r.CatalogSourceName, r.ClusterCatalogName) + } + } + + if len(adopted) > 0 { + fmt.Printf("\n✅ Adopted existing (%d):\n", len(adopted)) + for _, r := range adopted { + fmt.Printf(" ✓ %s/%s → ClusterCatalog/%s\n", + r.CatalogSourceNamespace, r.CatalogSourceName, r.ClusterCatalogName) + } + } + + if len(skipped) > 0 { + fmt.Printf("\n⏭ Skipped (%d):\n", len(skipped)) + for _, r := range skipped { + fmt.Printf(" - %s/%s: %s\n", + r.CatalogSourceNamespace, r.CatalogSourceName, r.Reason) + } + } + + if len(errored) > 0 { + fmt.Printf("\n❌ Errors (%d):\n", len(errored)) + for _, r := range errored { + fmt.Printf(" ✗ %s/%s: %s\n", + r.CatalogSourceNamespace, r.CatalogSourceName, r.Reason) + } + return fmt.Errorf("%d catalog source(s) failed to migrate", len(errored)) + } + + total := len(created) + len(adopted) + if total > 0 { + fmt.Printf("\n✅ Done: %d ClusterCatalog(s) ready\n\n", total) + } else if dryRun { + fmt.Printf("\nDry run complete.\n\n") + } else { + fmt.Printf("\nNo migratable CatalogSources found.\n\n") + } + + return nil +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/check.go b/migration/examples/cmd/migrate-operators-v0-to-v1/check.go new file mode 100644 index 0000000..d929427 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/check.go @@ -0,0 +1,122 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +var ( + checkSubscriptionName string + checkSubscriptionNamespace string + checkAll bool +) + +var checkCmd = &cobra.Command{ + Use: "check [operator-name]", + Short: "Check readiness and compatibility without performing migration", + Long: `Runs all pre-migration checks (readiness and compatibility) and reports +any issues that would prevent migration. Does not modify any cluster resources. + +Target is a Subscription name (with -n namespace), or --all to scan the cluster. + +Examples: + migrate-operators-v0-to-v1 check my-operator -n operators + migrate-operators-v0-to-v1 check --all`, + Args: cobra.MaximumNArgs(1), + RunE: runCheck, +} + +func init() { + checkCmd.Flags().StringVarP(&checkSubscriptionNamespace, "namespace", "n", "", "Subscription namespace (required without --all)") + checkCmd.Flags().BoolVar(&checkAll, "all", false, "Check all Subscriptions on the cluster") +} + +func runCheck(cmd *cobra.Command, args []string) error { + if checkAll && len(args) > 0 { + return fmt.Errorf("cannot specify both an operator name and --all") + } + if !checkAll && len(args) == 0 { + return fmt.Errorf("specify an operator name or --all") + } + + c, restCfg, err := newClient() + if err != nil { + return err + } + + m := migration.NewMigrator(c, restCfg) + m.Progress = progressFunc + ctx := cmd.Context() + + if checkAll { + fmt.Printf("\n%s%s🔎 Scanning all Subscriptions...%s\n", colorBold, colorCyan, colorReset) + startProgress() + results, err := m.ScanAllSubscriptions(ctx) + clearProgress() + if err != nil { + return fmt.Errorf("scan failed: %w", err) + } + migration.PrintScanSummary(results, func(format string, a ...interface{}) { + fmt.Printf(format, a...) + }) + return nil + } + + operatorName := args[0] + if checkSubscriptionNamespace == "" { + return fmt.Errorf("-n/--namespace is required") + } + + fmt.Printf("\n%s%s🔍 Pre-migration checks for %s/%s%s\n", colorBold, colorCyan, checkSubscriptionNamespace, operatorName, colorReset) + + opts := migration.Options{ + SubscriptionName: operatorName, + SubscriptionNamespace: checkSubscriptionNamespace, + } + opts.ApplyDefaults() + + sectionHeader("Readiness Checks") + readiness, readinessErr := m.CheckReadiness(ctx, opts) + if readinessErr != nil { + fail(fmt.Sprintf("Could not run readiness checks: %v", readinessErr)) + } else { + printCheckResults(readiness.Checks) + } + + sectionHeader("Compatibility Checks") + _, csv, _, profileErr := m.GetCSVAndInstallPlan(ctx, opts) + if profileErr != nil { + fail(fmt.Sprintf("Could not profile operator: %v", profileErr)) + } else { + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, compatErr := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if compatErr != nil { + fail(fmt.Sprintf("Could not run compatibility checks: %v", compatErr)) + } else { + printCheckResults(compat.Checks) + } + + sectionHeader("ClusterCatalog Availability") + bundleInfo, _ := m.GetBundleInfo(ctx, opts, csv, nil) + if bundleInfo != nil { + catalogName, catalogErr := m.ResolveClusterCatalog(ctx, bundleInfo, restCfg) + if catalogErr != nil { + var notFound *migration.PackageNotFoundError + if errors.As(catalogErr, ¬Found) { + warn(fmt.Sprintf("No ClusterCatalog found for package %q — run migrate-catalogs-v0-to-v1 first", bundleInfo.PackageName)) + } else { + warn(fmt.Sprintf("Catalog resolution error: %v", catalogErr)) + } + } else { + success(fmt.Sprintf("ClusterCatalog available: %s", catalogName)) + } + } + } + + fmt.Println() + return nil +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go b/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go new file mode 100644 index 0000000..9f8724a --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/operator-framework/library-olm/migration/pkg/migration" + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +var cleanupAll bool + +var cleanupCmd = &cobra.Command{ + Use: "cleanup [ce-name]", + Short: "Finish a partial migration (Conflict state)", + Long: `Resolves a Conflict state by deleting the Subscription and OLMv0 artifacts, +leaving the ClusterExtension intact. + +Use this when both a Subscription and an annotated ClusterExtension exist, +indicating a failed cleanup from a previous migration attempt. + +Target is a ClusterExtension name, or --all to cleanup all conflicts. + +Examples: + migrate-operators-v0-to-v1 cleanup my-operator + migrate-operators-v0-to-v1 cleanup --all`, + Args: cobra.MaximumNArgs(1), + RunE: runCleanup, +} + +func init() { + cleanupCmd.Flags().BoolVar(&cleanupAll, "all", false, "Cleanup all Conflict-state ClusterExtensions") +} + +func runCleanup(cmd *cobra.Command, args []string) error { + if cleanupAll && len(args) > 0 { + return fmt.Errorf("cannot specify both a CE name and --all") + } + if !cleanupAll && len(args) == 0 { + return fmt.Errorf("specify a ClusterExtension name or --all") + } + + c, restCfg, err := newClient() + if err != nil { + return err + } + + m := migration.NewMigrator(c, restCfg) + m.Progress = progressFunc + ctx := cmd.Context() + + if cleanupAll { + // Find all CEs that are in Conflict state + results, err := m.ScanAllSubscriptions(ctx) + if err != nil { + return fmt.Errorf("scan failed: %w", err) + } + + // Also scan CEs for those with the annotation + var ceList ocv1.ClusterExtensionList + if err := c.List(ctx, &ceList); err != nil { + return fmt.Errorf("failed to list ClusterExtensions: %w", err) + } + + var conflictCEs []string + for _, r := range results { + if r.Status == migration.OperatorStatusConflict { + // Find the CE name from the annotation + subRef := fmt.Sprintf("%s/%s", r.SubscriptionNamespace, r.SubscriptionName) + for _, ce := range ceList.Items { + if ce.Annotations[migration.MigratedFromSubscriptionAnnotation] == subRef { + conflictCEs = append(conflictCEs, ce.Name) + break + } + } + } + } + + if len(conflictCEs) == 0 { + info("No Conflict-state ClusterExtensions found.") + return nil + } + + fmt.Printf("\nCleaning up %d Conflict-state ClusterExtension(s)...\n", len(conflictCEs)) + var firstErr error + for _, ceName := range conflictCEs { + if err := m.CleanupConflict(ctx, ceName); err != nil { + fail(fmt.Sprintf("%s: %v", ceName, err)) + if firstErr == nil { + firstErr = err + } + } else { + success(fmt.Sprintf("%s conflict resolved", ceName)) + } + } + return firstErr + } + + ceName := args[0] + fmt.Printf("\n%s%s🧹 Cleaning up Conflict for %s...%s\n", colorBold, colorCyan, ceName, colorReset) + + if err := m.CleanupConflict(ctx, ceName); err != nil { + fail(fmt.Sprintf("Cleanup failed: %v", err)) + return err + } + + success(fmt.Sprintf("Conflict resolved for %s; OLMv0 artifacts removed, ClusterExtension intact", ceName)) + fmt.Println() + return nil +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go new file mode 100644 index 0000000..7e987a1 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go @@ -0,0 +1,306 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +var ( + convertNamespace string + convertAll bool + convertDryRun bool + convertContinueOnErr bool + convertBackupDir string + convertDeleteOG bool + convertCEName string + convertInstallNs string + + // Acknowledgment flags + convertAckWatchScope bool + convertAckOpCond bool + convertAckOLMv0API bool + convertAckScopedSA bool + convertAckNotSteady bool +) + +var convertCmd = &cobra.Command{ + Use: "convert [operator-name]", + Short: "Migrate an OLMv0 operator to OLMv1", + Long: `Migrates an OLMv0 Subscription/CSV to OLMv1 ClusterExtension/ClusterObjectSet. + +Use --dry-run to preview without making changes. +Use --all to migrate all eligible operators. + +Target is a Subscription name (with -n namespace), or --all. + +Examples: + migrate-operators-v0-to-v1 convert my-operator -n operators + migrate-operators-v0-to-v1 convert my-operator -n operators --dry-run + migrate-operators-v0-to-v1 convert --all + migrate-operators-v0-to-v1 convert --all --continue-on-error`, + Args: cobra.MaximumNArgs(1), + RunE: runConvert, +} + +func init() { + convertCmd.Flags().StringVarP(&convertNamespace, "namespace", "n", "", "Subscription namespace (required without --all)") + convertCmd.Flags().BoolVar(&convertAll, "all", false, "Migrate all eligible operators") + convertCmd.Flags().BoolVar(&convertDryRun, "dry-run", false, "Preview what would be migrated without making changes") + convertCmd.Flags().BoolVar(&convertContinueOnErr, "continue-on-error", false, "Continue migrating other operators when one fails (--all only)") + convertCmd.Flags().StringVar(&convertBackupDir, "backup", "", "Directory to write OLM resource backups before deletion") + convertCmd.Flags().BoolVar(&convertDeleteOG, "delete-operatorgroup", false, "Delete the OperatorGroup when no other Subscriptions remain") + convertCmd.Flags().StringVar(&convertCEName, "ce-name", "", "ClusterExtension name (default: Subscription name)") + convertCmd.Flags().StringVar(&convertInstallNs, "install-namespace", "", "Install namespace (default: Subscription namespace)") + convertCmd.Flags().BoolVar(&convertAckWatchScope, "acknowledge-watch-scope-change", false, "Acknowledge that the operator will run AllNamespaces (was scoped)") + convertCmd.Flags().BoolVar(&convertAckOpCond, "acknowledge-operator-condition", false, "Acknowledge active OperatorCondition usage") + convertCmd.Flags().BoolVar(&convertAckOLMv0API, "acknowledge-olmv0-api-access", false, "Acknowledge OLMv0 API RBAC without OLMv1 equivalent") + convertCmd.Flags().BoolVar(&convertAckScopedSA, "acknowledge-scoped-serviceaccount", false, "Acknowledge scoped OperatorGroup ServiceAccount (will use cluster-admin)") + convertCmd.Flags().BoolVar(&convertAckNotSteady, "acknowledge-not-steady-state", false, "Acknowledge that the operator is not at steady state") +} + +func runConvert(cmd *cobra.Command, args []string) error { + if convertAll && len(args) > 0 { + return fmt.Errorf("cannot specify both an operator name and --all") + } + if !convertAll && len(args) == 0 { + return fmt.Errorf("specify an operator name or --all") + } + + c, restCfg, err := newClient() + if err != nil { + return err + } + + m := migration.NewMigrator(c, restCfg) + m.Progress = progressFunc + ctx := cmd.Context() + + if convertAll { + fmt.Printf("\n%s%s🔎 Scanning all Subscriptions for migration...%s\n", colorBold, colorCyan, colorReset) + startProgress() + results, err := m.ScanAllSubscriptions(ctx) + clearProgress() + if err != nil { + return fmt.Errorf("scan failed: %w", err) + } + + migration.PrintScanSummary(results, func(format string, a ...interface{}) { + fmt.Printf(format, a...) + }) + + eligible := migration.EligibleFromScan(results) + if len(eligible) == 0 { + info("No eligible operators to migrate.") + return nil + } + + fmt.Printf("\n%s%sMigrating %d eligible operator(s)...%s\n", colorBold, colorCyan, len(eligible), colorReset) + + var firstErr error + for _, r := range eligible { + info(fmt.Sprintf("Migrating %s/%s...", r.SubscriptionNamespace, r.SubscriptionName)) + opts := migration.Options{ + SubscriptionName: r.SubscriptionName, + SubscriptionNamespace: r.SubscriptionNamespace, + } + opts.ApplyDefaults() + + if err := m.Migrate(ctx, opts); err != nil { + fail(fmt.Sprintf("%s/%s: %v", r.SubscriptionNamespace, r.SubscriptionName, err)) + if !convertContinueOnErr { + return err + } + if firstErr == nil { + firstErr = err + } + } else { + success(fmt.Sprintf("%s/%s migrated", r.SubscriptionNamespace, r.SubscriptionName)) + } + } + return firstErr + } + + // Single operator + operatorName := args[0] + if convertNamespace == "" { + return fmt.Errorf("-n/--namespace is required") + } + + opts := migration.Options{ + SubscriptionName: operatorName, + SubscriptionNamespace: convertNamespace, + ClusterExtensionName: convertCEName, + InstallNamespace: convertInstallNs, + } + opts.ApplyDefaults() + + if convertDryRun { + return runConvertDryRun(cmd, m, opts, restCfg) + } + + fmt.Printf("\n%s%s🔄 Migrating %s/%s to OLMv1...%s\n", colorBold, colorCyan, convertNamespace, operatorName, colorReset) + + stepHeader(1, "Profiling operator") + _, csv, ip, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + return fmt.Errorf("failed to profile operator: %w", err) + } + bundleInfo, err := m.GetBundleInfo(ctx, opts, csv, ip) + if err != nil { + return fmt.Errorf("failed to get bundle info: %w", err) + } + detail("Package:", bundleInfo.PackageName) + detail("Version:", bundleInfo.Version) + detail("Channel:", valueOrDefault(bundleInfo.Channel, "(default)")) + success("Operator profiled") + + stepHeader(2, "Checking readiness and compatibility") + sectionHeader("Readiness") + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + return fmt.Errorf("readiness check failed: %w", err) + } + printCheckResults(readiness.Checks) + + sectionHeader("Compatibility") + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if err != nil { + return fmt.Errorf("compatibility check failed: %w", err) + } + printCheckResults(compat.Checks) + + allFailed := append(readiness.FailedChecks(), compat.FailedChecks()...) + if len(allFailed) > 0 { + return fmt.Errorf("operator is not eligible for migration (%d checks failed)", len(allFailed)) + } + + stepHeader(3, "Determining target ClusterCatalog") + startProgress() + catalogName, err := m.ResolveClusterCatalog(ctx, bundleInfo, restCfg) + clearProgress() + if err != nil { + var notFound *migration.PackageNotFoundError + if errors.As(err, ¬Found) { + fail(fmt.Sprintf("No ClusterCatalog found for package %q — run migrate-catalogs-v0-to-v1 first", bundleInfo.PackageName)) + } + return fmt.Errorf("failed to resolve ClusterCatalog: %w", err) + } + bundleInfo.ResolvedCatalogName = catalogName + success(fmt.Sprintf("Selected ClusterCatalog: %s", catalogName)) + + stepHeader(4, "Collecting operator resources") + objects, err := m.CollectResources(ctx, opts, csv, ip, bundleInfo.PackageName) + if err != nil { + return fmt.Errorf("failed to collect resources: %w", err) + } + bundleInfo.CollectedObjects = objects + kindCounts := make(map[string]int) + for _, obj := range objects { + kindCounts[obj.GetKind()]++ + } + success(fmt.Sprintf("Found %d resources across %d kinds", len(objects), len(kindCounts))) + + stepHeader(5, "Backing up resources") + backup, err := m.BackupResources(ctx, opts, csv) + if err != nil { + return fmt.Errorf("failed to backup resources: %w", err) + } + _ = backup + success("Resources backed up in memory") + + stepHeader(6, "Preparing operator for migration") + info("Deleting Subscription and CSV (orphan cascade — workloads keep running)...") + if err := m.PrepareForMigration(ctx, opts, csv); err != nil { + return fmt.Errorf("preparation failed: %w", err) + } + success("OLMv0 management removed") + + stepHeader(7, "Creating ClusterObjectSet") + info(fmt.Sprintf("Applying COS %s-1 with %d objects...", opts.ClusterExtensionName, len(bundleInfo.CollectedObjects))) + startProgress() + if err := m.CreateClusterObjectSet(ctx, opts, bundleInfo); err != nil { + clearProgress() + return fmt.Errorf("COS creation failed: %w", err) + } + clearProgress() + success(fmt.Sprintf("ClusterObjectSet %s-1 reached Succeeded=True", opts.ClusterExtensionName)) + + stepHeader(8, "Creating ClusterExtension") + startProgress() + if err := m.CreateClusterExtension(ctx, opts, bundleInfo); err != nil { + clearProgress() + return fmt.Errorf("failed to create ClusterExtension: %w", err) + } + clearProgress() + success(fmt.Sprintf("ClusterExtension %s is Installed", opts.ClusterExtensionName)) + + stepHeader(9, "Cleaning up OLMv0 resources") + cleanupResult := m.CleanupOLMv0Resources(ctx, opts, bundleInfo.PackageName, csv.Name) + for _, action := range cleanupResult.Actions { + switch { + case action.Skipped: + info(fmt.Sprintf("⏭ %s", action.Description)) + case action.Error != nil: + warn(fmt.Sprintf("%s: %v", action.Description, action.Error)) + case action.Succeeded: + success(action.Description) + } + } + + banner(fmt.Sprintf("Migration complete! %s is now managed by OLMv1", bundleInfo.PackageName)) + fmt.Println() + return nil +} + +func runConvertDryRun(cmd *cobra.Command, m *migration.Migrator, opts migration.Options, restCfg interface{}) error { + ctx := cmd.Context() + fmt.Printf("\n%s%s🔍 Dry run: %s/%s%s\n", colorBold, colorCyan, opts.SubscriptionNamespace, opts.SubscriptionName, colorReset) + + info, err := m.GatherMigrationInfo(ctx, opts) + if err != nil { + return fmt.Errorf("failed to gather migration info: %w", err) + } + + success(fmt.Sprintf("Package: %s Version: %s Channel: %s", info.PackageName, info.Version, valueOrDefault(info.Channel, "(default)"))) + fmt.Printf("\n Resources that would be placed into ClusterObjectSet %s-1:\n", opts.ClusterExtensionName) + + kindCounts := make(map[string]int) + for _, obj := range info.CollectedObjects { + kindCounts[obj.GetKind()]++ + } + for kind, count := range kindCounts { + detail(fmt.Sprintf("%s:", kind), fmt.Sprintf("%d object(s)", count)) + } + + fmt.Printf("\n ClusterExtension that would be created:\n") + detail("Name:", opts.ClusterExtensionName) + detail("Namespace:", opts.InstallNamespace) + detail("PackageName:", info.PackageName) + if info.ManualApproval { + detail("Version:", fmt.Sprintf("%s (pinned — manual approval)", info.Version)) + } else { + detail("Version:", "(unset — automatic channel-based upgrades)") + } + detail("Channel:", valueOrDefault(info.Channel, "(none set)")) + detail("CollisionProtection:", "IfNoController") + + fmt.Println() + info2("No cluster resources were modified (dry run).") + return nil +} + +func info2(msg string) { + fmt.Printf(" %s\n", msg) +} + +func valueOrDefault(s, def string) string { + if s == "" { + return def + } + return s +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/main.go b/migration/examples/cmd/migrate-operators-v0-to-v1/main.go new file mode 100644 index 0000000..11f4f95 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/main.go @@ -0,0 +1,90 @@ +// migrate-operators-v0-to-v1 is a CLI tool for migrating operators managed by +// OLMv0 (Subscription/CSV) to OLMv1 (ClusterExtension/ClusterObjectSet). +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "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" + + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(ocv1.AddToScheme(scheme)) + utilruntime.Must(appsv1.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(operatorsv1.AddToScheme(scheme)) + utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) +} + +var kubeconfig string + +var rootCmd = &cobra.Command{ + Use: "migrate-operators-v0-to-v1", + Short: "Migrate OLMv0-managed operators to OLMv1", + Long: `migrate-operators-v0-to-v1 migrates operators from OLMv0 (Subscription/CSV) +to OLMv1 (ClusterExtension/ClusterObjectSet). + +Run migrate-catalogs-v0-to-v1 first to create ClusterCatalogs from CatalogSources. + +Subcommands: + check — report readiness and compatibility (no changes) + convert — perform the migration + rollback — restore an operator to OLMv0 management + cleanup — finish a partial migration (Conflict state)`, +} + +func init() { + rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file (default: KUBECONFIG env or ~/.kube/config)") + + rootCmd.AddCommand(checkCmd) + rootCmd.AddCommand(convertCmd) + rootCmd.AddCommand(rollbackCmd) + rootCmd.AddCommand(cleanupCmd) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func newClient() (client.Client, *rest.Config, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfig != "" { + loadingRules.ExplicitPath = kubeconfig + } + + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}, + ) + + restConfig, err := kubeConfig.ClientConfig() + if err != nil { + return nil, nil, fmt.Errorf("failed to get REST config: %w", err) + } + + c, err := client.New(restConfig, client.Options{Scheme: scheme}) + if err != nil { + return nil, nil, fmt.Errorf("failed to create client: %w", err) + } + return c, restConfig, nil +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/output.go b/migration/examples/cmd/migrate-operators-v0-to-v1/output.go new file mode 100644 index 0000000..22ba1c5 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/output.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" + colorBold = "\033[1m" + colorDim = "\033[2m" +) + +var ( + progressMu sync.Mutex + progressRunning bool + progressMsg string +) + +func progressFunc(msg string) { + progressMu.Lock() + defer progressMu.Unlock() + progressMsg = msg + if progressRunning { + fmt.Printf("\r %s%s...%s", colorDim, msg, colorReset) + } +} + +func startProgress() { + progressMu.Lock() + progressRunning = true + progressMu.Unlock() +} + +func clearProgress() { + progressMu.Lock() + progressRunning = false + if progressMsg != "" { + fmt.Printf("\r%80s\r", "") // clear the line + } + progressMsg = "" + progressMu.Unlock() +} + +func stepHeader(n int, title string) { + fmt.Printf("\n%s%sStep %d: %s%s\n", colorBold, colorCyan, n, title, colorReset) +} + +func sectionHeader(title string) { + fmt.Printf("\n %s%s%s\n", colorBold, title, colorReset) +} + +func banner(msg string) { + fmt.Printf("\n%s%s✅ %s%s\n", colorBold, colorGreen, msg, colorReset) +} + +func success(msg string) { + fmt.Printf(" %s✓%s %s\n", colorGreen, colorReset, msg) +} + +func fail(msg string) { + fmt.Printf(" %s✗%s %s\n", colorRed, colorReset, msg) +} + +func warn(msg string) { + fmt.Printf(" %s⚠%s %s\n", colorYellow, colorReset, msg) +} + +func info(msg string) { + fmt.Printf(" %s\n", msg) +} + +func detail(key, value string) { + fmt.Printf(" %s%-22s%s %s\n", colorDim, key, colorReset, value) +} + +func resource(kind, namespace, name string) { + fmt.Printf(" %s%s%s %s/%s\n", colorDim, kind, colorReset, namespace, name) +} + +func printCheckResults(checks []migration.CheckResult) { + for _, c := range checks { + if c.Passed { + success(fmt.Sprintf("%-35s %s%s%s", c.Name, colorDim, c.Message, colorReset)) + } else { + fail(fmt.Sprintf("%-35s %s", c.Name, c.Message)) + } + } +} diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go b/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go new file mode 100644 index 0000000..fc64030 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/operator-framework/library-olm/migration/pkg/migration" + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + rollbackAll bool + rollbackAcknowledgeInstalled bool +) + +var rollbackCmd = &cobra.Command{ + Use: "rollback [ce-name]", + Short: "Restore an operator to OLMv0 management", + Long: `Deletes the ClusterExtension and ClusterObjectSet (orphan cascade), +then restores the Subscription from the backup annotation. + +Requires --acknowledge-installed when the ClusterExtension is Installed=True. + +Target is a ClusterExtension name, or --all to rollback all migrated CEs. + +Examples: + migrate-operators-v0-to-v1 rollback my-operator --acknowledge-installed + migrate-operators-v0-to-v1 rollback --all --acknowledge-installed`, + Args: cobra.MaximumNArgs(1), + RunE: runRollback, +} + +func init() { + rollbackCmd.Flags().BoolVar(&rollbackAll, "all", false, "Rollback all migrated ClusterExtensions") + rollbackCmd.Flags().BoolVar(&rollbackAcknowledgeInstalled, "acknowledge-installed", false, "Confirm rollback even when CE is Installed=True") +} + +func runRollback(cmd *cobra.Command, args []string) error { + if rollbackAll && len(args) > 0 { + return fmt.Errorf("cannot specify both a CE name and --all") + } + if !rollbackAll && len(args) == 0 { + return fmt.Errorf("specify a ClusterExtension name or --all") + } + + c, restCfg, err := newClient() + if err != nil { + return err + } + + m := migration.NewMigrator(c, restCfg) + m.Progress = progressFunc + ctx := cmd.Context() + + if rollbackAll { + var ceList ocv1.ClusterExtensionList + if err := c.List(ctx, &ceList); err != nil { + return fmt.Errorf("failed to list ClusterExtensions: %w", err) + } + + var targets []string + for _, ce := range ceList.Items { + if _, ok := ce.Annotations[migration.MigratedFromSubscriptionAnnotation]; ok { + targets = append(targets, ce.Name) + } + } + + if len(targets) == 0 { + info("No migrated ClusterExtensions found.") + return nil + } + + fmt.Printf("\nRolling back %d migrated ClusterExtension(s)...\n", len(targets)) + var firstErr error + for _, name := range targets { + if err := m.RollbackClusterExtension(ctx, name, rollbackAcknowledgeInstalled); err != nil { + fail(fmt.Sprintf("%s: %v", name, err)) + if firstErr == nil { + firstErr = err + } + } else { + success(fmt.Sprintf("%s rolled back", name)) + } + } + return firstErr + } + + ceName := args[0] + fmt.Printf("\n%s%s🔄 Rolling back ClusterExtension %s...%s\n", colorBold, colorCyan, ceName, colorReset) + + if err := m.RollbackClusterExtension(ctx, ceName, rollbackAcknowledgeInstalled); err != nil { + fail(fmt.Sprintf("Rollback failed: %v", err)) + return err + } + + success(fmt.Sprintf("ClusterExtension %s rolled back; Subscription restored", ceName)) + fmt.Println() + return nil +} + +// rollbackSingleCE is a helper used when we have the CE object in hand. +func rollbackSingleCE(ctx interface{}, c client.Client, ce *ocv1.ClusterExtension, acknowledgeInstalled bool) error { + _ = c + _ = ce + _ = ctx + _ = acknowledgeInstalled + return nil +} diff --git a/migration/pkg/catalogmigration/catalogmigration.go b/migration/pkg/catalogmigration/catalogmigration.go new file mode 100644 index 0000000..88513a0 --- /dev/null +++ b/migration/pkg/catalogmigration/catalogmigration.go @@ -0,0 +1,384 @@ +// Package catalogmigration provides an API for migrating OLMv0 CatalogSources +// to OLMv1 ClusterCatalogs. +package catalogmigration + +import ( + "context" + "fmt" + "math" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +const ( + // MigratedFromCatalogSourceAnnotation is set on ClusterCatalog when first created or adopted. + MigratedFromCatalogSourceAnnotation = "olm.operatorframework.io/migrated-from-catalogsource" + + defaultPollMinutes = 15 +) + +// CatalogMigratorOptions configures the catalog migration. +type CatalogMigratorOptions struct { + DryRun bool + DeleteCatalogSource bool + AcknowledgePriorityOverflow bool +} + +// CatalogMigrationResult describes the outcome for a single CatalogSource. +type CatalogMigrationResult struct { + CatalogSourceName string + CatalogSourceNamespace string + ClusterCatalogName string + Status string // "created", "adopted", "skipped", "error", "dry-run" + Reason string +} + +// CatalogMigrator migrates OLMv0 CatalogSources to OLMv1 ClusterCatalogs. +type CatalogMigrator struct { + Client client.Client +} + +// NewCatalogMigrator creates a new CatalogMigrator. +func NewCatalogMigrator(c client.Client) *CatalogMigrator { + return &CatalogMigrator{Client: c} +} + +// MigrateCatalogs processes all CatalogSources across all namespaces and maps them to ClusterCatalogs. +// Strategy (per R8): +// - Same name + same image across namespaces → consolidate into a single ClusterCatalog +// - Same name + different image across namespaces → use - for each +// - Unique name → use metadata.name directly +func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigratorOptions) ([]CatalogMigrationResult, error) { + // List all CatalogSources across all namespaces + var csList operatorsv1alpha1.CatalogSourceList + if err := cm.Client.List(ctx, &csList); err != nil { + return nil, fmt.Errorf("failed to list CatalogSources: %w", err) + } + + // List all existing ClusterCatalogs + var ccList ocv1.ClusterCatalogList + if err := cm.Client.List(ctx, &ccList); err != nil { + return nil, fmt.Errorf("failed to list ClusterCatalogs: %w", err) + } + + // Build map of existing ClusterCatalogs by image ref + existingByImage := make(map[string]*ocv1.ClusterCatalog) + for i := range ccList.Items { + cc := &ccList.Items[i] + if cc.Spec.Source.Image != nil && cc.Spec.Source.Image.Ref != "" { + existingByImage[cc.Spec.Source.Image.Ref] = cc + } + } + + // List all Subscriptions to detect which CatalogSources are still referenced + var subList operatorsv1alpha1.SubscriptionList + if err := cm.Client.List(ctx, &subList); err != nil { + return nil, fmt.Errorf("failed to list Subscriptions: %w", err) + } + + // Build set of referenced CatalogSources + referencedCS := make(map[string]bool) + for _, sub := range subList.Items { + key := fmt.Sprintf("%s/%s", sub.Spec.CatalogSourceNamespace, sub.Spec.CatalogSource) + referencedCS[key] = true + } + + // Determine naming strategy: group by name, check for image conflicts + type csEntry struct { + cs operatorsv1alpha1.CatalogSource + image string + } + byName := make(map[string][]csEntry) + for _, cs := range csList.Items { + if cs.Spec.SourceType != operatorsv1alpha1.SourceTypeGrpc || cs.Spec.Image == "" { + continue // non-image sources handled separately below + } + byName[cs.Name] = append(byName[cs.Name], csEntry{cs: cs, image: cs.Spec.Image}) + } + + // For each name, determine if all entries share the same image + nameStrategy := make(map[string]string) // cs name → "shared" or "namespace" + for name, entries := range byName { + allSame := true + firstImage := entries[0].image + for _, e := range entries[1:] { + if e.image != firstImage { + allSame = false + break + } + } + if allSame { + nameStrategy[name] = "shared" + } else { + nameStrategy[name] = "namespace" + } + } + + var results []CatalogMigrationResult + + // Process non-image CatalogSources + for _, cs := range csList.Items { + if cs.Spec.SourceType == operatorsv1alpha1.SourceTypeGrpc && cs.Spec.Image != "" { + continue // handled in the main loop below + } + reason := "" + switch { + case cs.Spec.SourceType == operatorsv1alpha1.SourceTypeConfigmap: + reason = "configmap-type CatalogSource has no OLMv1 equivalent" + case cs.Spec.SourceType == operatorsv1alpha1.SourceTypeInternal: + reason = "internal-type CatalogSource has no OLMv1 equivalent" + case cs.Spec.SourceType == operatorsv1alpha1.SourceTypeGrpc && cs.Spec.Image == "": + reason = "grpc address-only CatalogSource (no spec.image) has no OLMv1 equivalent" + default: + reason = fmt.Sprintf("unsupported sourceType %q", cs.Spec.SourceType) + } + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + Status: "skipped", + Reason: reason, + }) + } + + // Track which ClusterCatalog names we've already created this run (for consolidation) + createdThisRun := make(map[string]bool) + + // Process image-type CatalogSources + for _, cs := range csList.Items { + if cs.Spec.SourceType != operatorsv1alpha1.SourceTypeGrpc || cs.Spec.Image == "" { + continue + } + + // Determine ClusterCatalog name + var ccName string + switch nameStrategy[cs.Name] { + case "shared": + ccName = cs.Name + default: + ccName = fmt.Sprintf("%s-%s", cs.Name, cs.Namespace) + } + + // Validate and convert priority + priority, priorityErr := validatePriority(cs.Spec.Priority, opts.AcknowledgePriorityOverflow) + if priorityErr != nil { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "skipped", + Reason: priorityErr.Error(), + }) + continue + } + + // Convert poll interval + pollMinutes := convertPollInterval(cs) + + csRef := fmt.Sprintf("%s/%s", cs.Namespace, cs.Name) + + // Check if already created this run (consolidation case) + if createdThisRun[ccName] { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "adopted", + Reason: fmt.Sprintf("consolidated into shared ClusterCatalog %s", ccName), + }) + continue + } + + // Check if an existing ClusterCatalog matches by image + if existing, found := existingByImage[cs.Spec.Image]; found { + // Adopt: set annotation if not already present + if opts.DryRun { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: existing.Name, + Status: "dry-run", + Reason: fmt.Sprintf("would adopt existing ClusterCatalog %s", existing.Name), + }) + continue + } + + if err := cm.annotateIfNotPresent(ctx, existing, csRef); err != nil { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: existing.Name, + Status: "error", + Reason: fmt.Sprintf("failed to annotate existing ClusterCatalog: %v", err), + }) + continue + } + + createdThisRun[existing.Name] = true + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: existing.Name, + Status: "adopted", + Reason: "existing ClusterCatalog with matching image adopted", + }) + + // Handle --delete-catalogsource + if opts.DeleteCatalogSource && !referencedCS[csRef] { + _ = cm.Client.Delete(ctx, &cs) + } + continue + } + + // Create new ClusterCatalog + if opts.DryRun { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "dry-run", + Reason: fmt.Sprintf("would create ClusterCatalog %s from image %s", ccName, cs.Spec.Image), + }) + continue + } + + imageSource := &ocv1.ImageSource{Ref: cs.Spec.Image} + if pollMinutes > 0 { + imageSource.PollIntervalMinutes = &pollMinutes + } + + cc := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: ccName, + Annotations: map[string]string{ + MigratedFromCatalogSourceAnnotation: csRef, + }, + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: imageSource, + }, + Priority: priority, + AvailabilityMode: ocv1.AvailabilityModeAvailable, + }, + } + + if err := cm.Client.Create(ctx, cc); err != nil { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "error", + Reason: fmt.Sprintf("failed to create ClusterCatalog: %v", err), + }) + continue + } + + // Wait for serving + if err := cm.waitForServing(ctx, ccName); err != nil { + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "error", + Reason: fmt.Sprintf("ClusterCatalog not serving: %v", err), + }) + continue + } + + createdThisRun[ccName] = true + existingByImage[cs.Spec.Image] = cc + + results = append(results, CatalogMigrationResult{ + CatalogSourceName: cs.Name, + CatalogSourceNamespace: cs.Namespace, + ClusterCatalogName: ccName, + Status: "created", + Reason: fmt.Sprintf("created from image %s", cs.Spec.Image), + }) + + // Handle --delete-catalogsource + if opts.DeleteCatalogSource && !referencedCS[csRef] { + _ = cm.Client.Delete(ctx, &cs) + } + } + + return results, nil +} + +// annotateIfNotPresent sets MigratedFromCatalogSourceAnnotation on the ClusterCatalog +// if it is not already present (idempotent). +func (cm *CatalogMigrator) annotateIfNotPresent(ctx context.Context, cc *ocv1.ClusterCatalog, csRef string) error { + if _, ok := cc.Annotations[MigratedFromCatalogSourceAnnotation]; ok { + return nil // already set, leave it unchanged + } + + patch := client.MergeFrom(cc.DeepCopy()) + if cc.Annotations == nil { + cc.Annotations = make(map[string]string) + } + cc.Annotations[MigratedFromCatalogSourceAnnotation] = csRef + return cm.Client.Patch(ctx, cc, patch) +} + +// waitForServing polls until the ClusterCatalog has Serving=True. +func (cm *CatalogMigrator) waitForServing(ctx context.Context, ccName string) error { + return wait.PollUntilContextTimeout(ctx, 5*time.Second, 3*time.Minute, true, func(ctx context.Context) (bool, error) { + var cc ocv1.ClusterCatalog + if err := cm.Client.Get(ctx, client.ObjectKey{Name: ccName}, &cc); err != nil { + return false, err + } + for _, c := range cc.Status.Conditions { + if c.Type == "Serving" && c.Status == metav1.ConditionTrue { + return true, nil + } + } + return false, nil + }) +} + +// validatePriority checks that the CatalogSource priority fits in int32 range. +// If it doesn't fit and AcknowledgePriorityOverflow is true, caps at MaxInt32/MinInt32. +func validatePriority(priority int, acknowledge bool) (int32, error) { + if priority > math.MaxInt32 || priority < math.MinInt32 { + if !acknowledge { + return 0, fmt.Errorf("spec.priority %d is out of int32 range; pass --acknowledge-priority-overflow to cap and proceed", priority) + } + if priority > math.MaxInt32 { + return math.MaxInt32, nil + } + return math.MinInt32, nil + } + return int32(priority), nil //nolint:gosec // validated above +} + +// convertPollInterval converts the CatalogSource registryPoll interval to integer minutes. +// Returns 0 if no interval is set, or if the image ref is digest-based (poll not allowed). +func convertPollInterval(cs operatorsv1alpha1.CatalogSource) int { + // Digest-based refs must not have a poll interval + if strings.Contains(cs.Spec.Image, "@sha256:") { + return 0 + } + + if cs.Spec.UpdateStrategy == nil || cs.Spec.UpdateStrategy.RegistryPoll == nil { + return 0 + } + + interval := cs.Spec.UpdateStrategy.RegistryPoll.Interval + if interval == nil || interval.Duration == 0 { + return 0 + } + + minutes := int(interval.Duration.Minutes()) + if minutes < 1 { + minutes = 1 + } + return minutes +} diff --git a/migration/pkg/migration/catalog.go b/migration/pkg/migration/catalog.go new file mode 100644 index 0000000..30e6a3d --- /dev/null +++ b/migration/pkg/migration/catalog.go @@ -0,0 +1,283 @@ +package migration + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + "k8s.io/client-go/transport" + "sigs.k8s.io/controller-runtime/pkg/client" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// catalogMeta represents a single entry from the catalog JSONL response. +type catalogMeta struct { + Schema string `json:"schema"` + Name string `json:"name"` + Package string `json:"package"` + Props json.RawMessage `json:"properties,omitempty"` + Entries []channelEntry `json:"entries,omitempty"` +} + +type channelEntry struct { + Name string `json:"name"` +} + +// CatalogPackageInfo holds the results of querying a catalog for a package. +type CatalogPackageInfo struct { + Found bool + AvailableVersions []string + AvailableChannels []string + VersionFound bool + ChannelFound bool +} + +// QueryCatalogForPackage queries a ClusterCatalog's content to check if the +// specified package, version, and channel are available. +func (m *Migrator) QueryCatalogForPackage(ctx context.Context, catalog *ocv1.ClusterCatalog, packageName, version, channel string, restConfig *rest.Config) (*CatalogPackageInfo, error) { + if catalog.Status.URLs == nil { + return nil, fmt.Errorf("catalog %s has no URLs in status", catalog.Name) + } + + proxyURL := fmt.Sprintf("%s/api/v1/namespaces/olmv1-system/services/https:catalogd-service:443/proxy/catalogs/%s/api/v1/all", + restConfig.Host, catalog.Name) + + transportConfig, err := restConfig.TransportConfig() + if err != nil { + return nil, fmt.Errorf("failed to get transport config: %w", err) + } + + rt, err := transport.New(transportConfig) + if err != nil { + return nil, fmt.Errorf("failed to create transport: %w", err) + } + + httpClient := &http.Client{Transport: rt} + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, proxyURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to query catalog: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("catalog returned status %d", resp.StatusCode) + } + + return parseCatalogResponse(resp.Body, packageName, version, channel) +} + +func parseCatalogResponse(body io.Reader, packageName, version, channel string) (*CatalogPackageInfo, error) { + info := &CatalogPackageInfo{} + versionSet := map[string]bool{} + channelSet := map[string]bool{} + + scanner := bufio.NewScanner(body) + scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024) + + for scanner.Scan() { + var meta catalogMeta + if err := json.Unmarshal(scanner.Bytes(), &meta); err != nil { + continue + } + + switch meta.Schema { + case "olm.package": + if meta.Name == packageName { + info.Found = true + } + case "olm.bundle": + if meta.Package != packageName { + continue + } + bundleVersion := extractBundleVersion(meta.Props) + if bundleVersion != "" { + versionSet[bundleVersion] = true + } + case "olm.channel": + if meta.Package != packageName { + continue + } + channelSet[meta.Name] = true + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading catalog response: %w", err) + } + + for v := range versionSet { + info.AvailableVersions = append(info.AvailableVersions, v) + } + for ch := range channelSet { + info.AvailableChannels = append(info.AvailableChannels, ch) + } + + info.VersionFound = versionSet[version] + info.ChannelFound = channel == "" || channelSet[channel] + + return info, nil +} + +func extractBundleVersion(propsRaw json.RawMessage) string { + if propsRaw == nil { + return "" + } + var props []struct { + Type string `json:"type"` + Value json.RawMessage `json:"value"` + } + if err := json.Unmarshal(propsRaw, &props); err != nil { + return "" + } + for _, p := range props { + if p.Type == "olm.package" { + var pkg struct { + Version string `json:"version"` + } + if err := json.Unmarshal(p.Value, &pkg); err == nil { + return pkg.Version + } + } + } + return "" +} + +// ResolveClusterCatalog finds a ClusterCatalog that serves the package at the installed version. +func (m *Migrator) ResolveClusterCatalog(ctx context.Context, info *MigrationInfo, restConfig *rest.Config) (string, error) { + var catalogList ocv1.ClusterCatalogList + if err := m.Client.List(ctx, &catalogList); err != nil { + return "", fmt.Errorf("failed to list ClusterCatalogs: %w", err) + } + + type catalogCandidate struct { + name string + priority int32 + pkgInfo *CatalogPackageInfo + } + var candidates []catalogCandidate + var queriedCatalogs []string + + for i := range catalogList.Items { + catalog := &catalogList.Items[i] + + if catalog.Spec.AvailabilityMode == ocv1.AvailabilityModeUnavailable { + continue + } + + serving := false + for _, c := range catalog.Status.Conditions { + if c.Type == "Serving" && c.Status == metav1.ConditionTrue { + serving = true + break + } + } + if !serving { + continue + } + + queriedCatalogs = append(queriedCatalogs, catalog.Name) + m.progress(fmt.Sprintf("Querying catalog %s for package %s@%s...", catalog.Name, info.PackageName, info.Version)) + + pkgInfo, err := m.QueryCatalogForPackage(ctx, catalog, info.PackageName, info.Version, info.Channel, restConfig) + if err != nil { + m.progress(fmt.Sprintf("Could not query catalog %s: %v", catalog.Name, err)) + continue + } + + if pkgInfo.Found && pkgInfo.VersionFound && pkgInfo.ChannelFound { + candidates = append(candidates, catalogCandidate{ + name: catalog.Name, + priority: catalog.Spec.Priority, + pkgInfo: pkgInfo, + }) + } + } + + if len(candidates) == 0 { + return "", &PackageNotFoundError{ + PackageName: info.PackageName, + Version: info.Version, + Channel: info.Channel, + QueriedCatalogs: queriedCatalogs, + } + } + + best := candidates[0] + for _, c := range candidates[1:] { + if c.priority > best.priority { + best = c + } + } + + return best.name, nil +} + +// PackageNotFoundError is returned when no ClusterCatalog contains the required package. +type PackageNotFoundError struct { + PackageName string + Version string + Channel string + QueriedCatalogs []string +} + +func (e *PackageNotFoundError) Error() string { + msg := fmt.Sprintf("package %q at version %q", e.PackageName, e.Version) + if e.Channel != "" { + msg += fmt.Sprintf(" in channel %q", e.Channel) + } + msg += " not found in any serving ClusterCatalog" + if len(e.QueriedCatalogs) > 0 { + msg += fmt.Sprintf(" (queried: %v)", e.QueriedCatalogs) + } + return msg +} + +// CreateClusterCatalog creates a ClusterCatalog from a CatalogSource image reference +// and waits for it to reach a serving state. +func (m *Migrator) CreateClusterCatalog(ctx context.Context, name, imageRef string) error { + catalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: imageRef, + }, + }, + }, + } + + if err := m.Client.Create(ctx, catalog); err != nil { + return fmt.Errorf("failed to create ClusterCatalog: %w", err) + } + + return wait.PollUntilContextTimeout(ctx, 5*time.Second, 3*time.Minute, true, func(ctx context.Context) (bool, error) { + var cat ocv1.ClusterCatalog + if err := m.Client.Get(ctx, client.ObjectKeyFromObject(catalog), &cat); err != nil { + return false, err + } + for _, c := range cat.Status.Conditions { + if c.Type == "Serving" && c.Status == metav1.ConditionTrue { + return true, nil + } + } + m.progress(fmt.Sprintf("Waiting for ClusterCatalog %s to become ready...", name)) + return false, nil + }) +} diff --git a/migration/pkg/migration/checks.go b/migration/pkg/migration/checks.go new file mode 100644 index 0000000..ae06703 --- /dev/null +++ b/migration/pkg/migration/checks.go @@ -0,0 +1,34 @@ +package migration + +// CheckResult represents the outcome of a single pre-migration check. +type CheckResult struct { + Name string // short name of the check + Passed bool + Message string // detail — pass reason or failure reason +} + +// PreMigrationReport contains the results of all readiness and compatibility checks. +type PreMigrationReport struct { + Checks []CheckResult +} + +// Passed returns true if all checks passed. +func (r *PreMigrationReport) Passed() bool { + for _, c := range r.Checks { + if !c.Passed { + return false + } + } + return true +} + +// FailedChecks returns only the checks that failed. +func (r *PreMigrationReport) FailedChecks() []CheckResult { + var failed []CheckResult + for _, c := range r.Checks { + if !c.Passed { + failed = append(failed, c) + } + } + return failed +} diff --git a/migration/pkg/migration/collector.go b/migration/pkg/migration/collector.go new file mode 100644 index 0000000..0d516f7 --- /dev/null +++ b/migration/pkg/migration/collector.go @@ -0,0 +1,433 @@ +package migration + +import ( + "context" + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +// possibleResourceGVKs lists all resource GVKs that may be part of an OLMv0 operator installation. +var possibleResourceGVKs = []schema.GroupVersionKind{ + {Group: "", Version: "v1", Kind: "Namespace"}, + {Group: "", Version: "v1", Kind: "Secret"}, + {Group: "", Version: "v1", Kind: "ConfigMap"}, + {Group: "", Version: "v1", Kind: "ServiceAccount"}, + {Group: "", Version: "v1", Kind: "Service"}, + {Group: "apps", Version: "v1", Kind: "Deployment"}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}, + {Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"}, + {Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"}, + {Group: "monitoring.coreos.com", Version: "v1", Kind: "PrometheusRule"}, + {Group: "monitoring.coreos.com", Version: "v1", Kind: "ServiceMonitor"}, + {Group: "monitoring.coreos.com", Version: "v1", Kind: "PodMonitor"}, + {Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"}, + {Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClass"}, + {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}, + {Group: "autoscaling.k8s.io", Version: "v1", Kind: "VerticalPodAutoscaler"}, + {Group: "console.openshift.io", Version: "v1", Kind: "ConsoleYAMLSample"}, + {Group: "console.openshift.io", Version: "v1", Kind: "ConsoleQuickStart"}, + {Group: "console.openshift.io", Version: "v1", Kind: "ConsoleCLIDownload"}, + {Group: "console.openshift.io", Version: "v1", Kind: "ConsoleLink"}, + {Group: "console.openshift.io", Version: "v1", Kind: "ConsolePlugin"}, +} + +// clusterScopedKinds is the set of kinds that are cluster-scoped (no namespace in lookups). +var clusterScopedKinds = map[string]bool{ + "Namespace": true, + "ClusterRole": true, + "ClusterRoleBinding": true, + "CustomResourceDefinition": true, + "PriorityClass": true, + "ConsoleYAMLSample": true, + "ConsoleQuickStart": true, + "ConsoleCLIDownload": true, + "ConsoleLink": true, + "ConsolePlugin": true, + "ValidatingWebhookConfiguration": true, + "MutatingWebhookConfiguration": true, +} + +// olmv0OnlyKinds are OLMv0 management resources that should not be included in the COS. +var olmv0OnlyKinds = map[string]bool{ + "OperatorCondition": true, + "Operator": true, + "OperatorGroup": true, +} + +// GetCSVAndInstallPlan retrieves the Subscription, CSV, and InstallPlan. +func (m *Migrator) GetCSVAndInstallPlan(ctx context.Context, opts Options) (*operatorsv1alpha1.Subscription, *operatorsv1alpha1.ClusterServiceVersion, *operatorsv1alpha1.InstallPlan, error) { + var sub operatorsv1alpha1.Subscription + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: opts.SubscriptionName, + Namespace: opts.SubscriptionNamespace, + }, &sub); err != nil { + return nil, nil, nil, fmt.Errorf("failed to get Subscription: %w", err) + } + + csvName := sub.Status.InstalledCSV + if csvName == "" { + return nil, nil, nil, fmt.Errorf("subscription has no installedCSV") + } + + var csv operatorsv1alpha1.ClusterServiceVersion + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: csvName, + Namespace: opts.SubscriptionNamespace, + }, &csv); err != nil { + return nil, nil, nil, fmt.Errorf("failed to get CSV %s: %w", csvName, err) + } + + var ip *operatorsv1alpha1.InstallPlan + if sub.Status.InstallPlanRef != nil { + ip = &operatorsv1alpha1.InstallPlan{} + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: sub.Status.InstallPlanRef.Name, + Namespace: sub.Status.InstallPlanRef.Namespace, + }, ip); err != nil { + return nil, nil, nil, fmt.Errorf("failed to get InstallPlan %s: %w", sub.Status.InstallPlanRef.Name, err) + } + } + + return &sub, &csv, ip, nil +} + +// GetBundleInfo extracts bundle metadata from the Subscription and CSV. +func (m *Migrator) GetBundleInfo(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion, ip *operatorsv1alpha1.InstallPlan) (*MigrationInfo, error) { + var sub operatorsv1alpha1.Subscription + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: opts.SubscriptionName, + Namespace: opts.SubscriptionNamespace, + }, &sub); err != nil { + return nil, fmt.Errorf("failed to get Subscription: %w", err) + } + + info := &MigrationInfo{ + PackageName: sub.Spec.Package, + Channel: sub.Spec.Channel, + ManualApproval: sub.Spec.InstallPlanApproval == operatorsv1alpha1.ApprovalManual, + CatalogSourceRef: types.NamespacedName{ + Name: sub.Spec.CatalogSource, + Namespace: sub.Spec.CatalogSourceNamespace, + }, + } + + info.BundleName = csv.Name + info.Version = parseCSVVersion(csv) + + if ip != nil { + for _, bl := range ip.Status.BundleLookups { + if bl.Identifier == csv.Name { + info.BundleImage = bl.Path + if bl.CatalogSourceRef != nil { + info.CatalogSourceRef = types.NamespacedName{ + Name: bl.CatalogSourceRef.Name, + Namespace: bl.CatalogSourceRef.Namespace, + } + } + break + } + } + } + + return info, nil +} + +// parseCSVVersion extracts the version from the CSV's operatorframework.io/properties annotation. +func parseCSVVersion(csv *operatorsv1alpha1.ClusterServiceVersion) string { + propsJSON := csv.Annotations["operatorframework.io/properties"] + if propsJSON == "" { + return csv.Spec.Version.String() + } + + props, err := parseProperties(propsJSON) + if err != nil { + return csv.Spec.Version.String() + } + + for _, p := range props { + if p.Type == "olm.package" { + var pkg struct { + PackageName string `json:"packageName"` + Version string `json:"version"` + } + if err := json.Unmarshal(p.Value, &pkg); err == nil && pkg.Version != "" { + return pkg.Version + } + } + } + return csv.Spec.Version.String() +} + +// GetCatalogSourceImage retrieves the image reference from the CatalogSource spec. +func (m *Migrator) GetCatalogSourceImage(ctx context.Context, csRef types.NamespacedName) (string, error) { + var cs operatorsv1alpha1.CatalogSource + if err := m.Client.Get(ctx, csRef, &cs); err != nil { + return "", fmt.Errorf("failed to get CatalogSource %s/%s: %w", csRef.Namespace, csRef.Name, err) + } + if cs.Spec.Image == "" { + return "", fmt.Errorf("CatalogSource %s/%s has no spec.image set", csRef.Namespace, csRef.Name) + } + return cs.Spec.Image, nil +} + +// CollectResources gathers all resources belonging to the operator using multiple collection strategies. +func (m *Migrator) CollectResources(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion, ip *operatorsv1alpha1.InstallPlan, packageName string) ([]unstructured.Unstructured, error) { + seen := make(map[string]bool) + var collected []unstructured.Unstructured + + addIfNew := func(obj unstructured.Unstructured) { + if olmv0OnlyKinds[obj.GetKind()] { + return + } + key := resourceKey(obj) + if !seen[key] { + seen[key] = true + collected = append(collected, obj) + } + } + + // Strategy 1: Operator CR status.components.refs (primary) + fromOperatorCR, _ := m.gatherResourcesFromOperatorCR(ctx, packageName, opts.SubscriptionNamespace) + for _, obj := range fromOperatorCR { + addIfNew(obj) + } + + // Strategy 2: CRDs by package label + crds, err := m.getCRDsByPackage(ctx, opts, packageName) + if err != nil { + return nil, fmt.Errorf("failed to collect CRDs by package: %w", err) + } + for _, obj := range crds { + addIfNew(obj) + } + + // Strategy 3: Resources by olm.owner label + for _, obj := range m.gatherResourcesByOwnerLabel(ctx, csv.Name) { + addIfNew(obj) + } + + // Strategy 4: Resources by ownerReference in the subscription namespace + for _, obj := range m.gatherResourcesByOwnerRef(ctx, opts.SubscriptionNamespace, csv) { + addIfNew(obj) + } + + // Strategy 5: Resources from InstallPlan steps + if ip != nil { + for _, obj := range m.gatherResourcesFromInstallPlan(ctx, ip, csv.Name) { + addIfNew(obj) + } + } + + return collected, nil +} + +func resourceKey(obj unstructured.Unstructured) string { + return fmt.Sprintf("%s/%s/%s/%s", + obj.GetObjectKind().GroupVersionKind().GroupKind().String(), + obj.GetNamespace(), + obj.GetName(), + obj.GetAPIVersion()) +} + +func (m *Migrator) getCRDsByPackage(ctx context.Context, opts Options, packageName string) ([]unstructured.Unstructured, error) { + packageLabel := fmt.Sprintf("operators.coreos.com/%s.%s", packageName, opts.SubscriptionNamespace) + + var crdList unstructured.UnstructuredList + crdList.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "apiextensions.k8s.io", + Version: "v1", + Kind: "CustomResourceDefinitionList", + }) + + if err := m.Client.List(ctx, &crdList, + client.MatchingLabels{ + "olm.managed": "true", + packageLabel: "", + }, + ); err != nil { + return nil, err + } + return crdList.Items, nil +} + +func (m *Migrator) gatherResourcesByOwnerLabel(ctx context.Context, csvName string) []unstructured.Unstructured { + var result []unstructured.Unstructured + + for _, gvk := range possibleResourceGVKs { + var list unstructured.UnstructuredList + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + }) + + if err := m.Client.List(ctx, &list, + client.MatchingLabels{ + "olm.managed": "true", + "olm.owner": csvName, + }, + ); err != nil { + continue + } + result = append(result, list.Items...) + } + return result +} + +func (m *Migrator) gatherResourcesByOwnerRef(ctx context.Context, namespace string, csv *operatorsv1alpha1.ClusterServiceVersion) []unstructured.Unstructured { + var result []unstructured.Unstructured + + for _, gvk := range possibleResourceGVKs { + if clusterScopedKinds[gvk.Kind] { + continue + } + + var list unstructured.UnstructuredList + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind + "List", + }) + + if err := m.Client.List(ctx, &list, client.InNamespace(namespace)); err != nil { + continue + } + + for _, obj := range list.Items { + for _, ref := range obj.GetOwnerReferences() { + if ref.Kind == "ClusterServiceVersion" && ref.Name == csv.Name { + result = append(result, obj) + break + } + } + } + } + return result +} + +func (m *Migrator) gatherResourcesFromInstallPlan(ctx context.Context, ip *operatorsv1alpha1.InstallPlan, csvName string) []unstructured.Unstructured { + var result []unstructured.Unstructured + + for _, step := range ip.Status.Plan { + if step == nil || step.Resolving != csvName { + continue + } + + res := step.Resource + if res.Kind == "ClusterServiceVersion" || res.Kind == "Subscription" || res.Kind == "InstallPlan" { + continue + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: res.Group, + Version: res.Version, + Kind: res.Kind, + }) + + nn := types.NamespacedName{Name: res.Name} + if !clusterScopedKinds[res.Kind] { + nn.Namespace = ip.Namespace + } + + if err := m.Client.Get(ctx, nn, obj); err != nil { + continue + } + result = append(result, *obj) + } + return result +} + +// gatherResourcesFromOperatorCR collects resources from the Operator CR's status.components.refs. +func (m *Migrator) gatherResourcesFromOperatorCR(ctx context.Context, packageName, namespace string) ([]unstructured.Unstructured, error) { + op, err := m.GetOperatorCR(ctx, packageName, namespace) + if err != nil { + return nil, err + } + + if op.Status.Components == nil { + return nil, nil + } + + skipKinds := map[string]bool{ + "ClusterServiceVersion": true, + "Subscription": true, + "InstallPlan": true, + } + + var result []unstructured.Unstructured + for _, ref := range op.Status.Components.Refs { + if ref.ObjectReference == nil { + continue + } + if skipKinds[ref.Kind] { + continue + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: ref.GroupVersionKind().Group, + Version: ref.GroupVersionKind().Version, + Kind: ref.Kind, + }) + + nn := types.NamespacedName{Name: ref.Name} + if ref.Namespace != "" { + nn.Namespace = ref.Namespace + } + + if err := m.Client.Get(ctx, nn, obj); err != nil { + continue + } + result = append(result, *obj) + } + return result, nil +} + +// GatherMigrationInfo profiles the operator and collects all migration information. +func (m *Migrator) GatherMigrationInfo(ctx context.Context, opts Options) (*MigrationInfo, error) { + _, csv, ip, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + return nil, err + } + + info, err := m.GetBundleInfo(ctx, opts, csv, ip) + if err != nil { + return nil, err + } + + csImage, err := m.GetCatalogSourceImage(ctx, info.CatalogSourceRef) + if err == nil { + info.CatalogSourceImage = csImage + } + + objects, err := m.CollectResources(ctx, opts, csv, ip, info.PackageName) + if err != nil { + return nil, err + } + info.CollectedObjects = objects + + return info, nil +} + +// GetOperatorCR retrieves the Operator CR for the given package and namespace. +func (m *Migrator) GetOperatorCR(ctx context.Context, packageName, namespace string) (*operatorsv1.Operator, error) { + operatorName := fmt.Sprintf("%s.%s", packageName, namespace) + var op operatorsv1.Operator + if err := m.Client.Get(ctx, types.NamespacedName{Name: operatorName}, &op); err != nil { + return nil, err + } + return &op, nil +} diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go new file mode 100644 index 0000000..b666423 --- /dev/null +++ b/migration/pkg/migration/compatibility.go @@ -0,0 +1,254 @@ +package migration + +import ( + "context" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +// CheckCompatibility runs all compatibility checks and returns a report with individual results. +func (m *Migrator) CheckCompatibility(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion, bundleProperties string) (*PreMigrationReport, error) { + report := &PreMigrationReport{} + + // OperatorGroup checks + ogChecks, err := m.checkAllNamespacesMode(ctx, opts) + if err != nil { + return nil, err + } + report.Checks = append(report.Checks, ogChecks...) + + // Dependency checks (C2 — hard block) + report.Checks = append(report.Checks, checkNoDependencies(bundleProperties)...) + + // APIService checks (C3 — hard block, temporary until OPRUN-4723) + report.Checks = append(report.Checks, checkNoAPIServices(csv)) + + // OperatorCondition checks (C4) + condCheck, err := m.checkNoOperatorConditions(ctx, opts, csv) + if err != nil { + return nil, err + } + report.Checks = append(report.Checks, condCheck) + + return report, nil +} + +func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([]CheckResult, error) { + var ogList operatorsv1.OperatorGroupList + if err := m.Client.List(ctx, &ogList, client.InNamespace(opts.SubscriptionNamespace)); err != nil { + return nil, fmt.Errorf("failed to list OperatorGroups in %s: %w", opts.SubscriptionNamespace, err) + } + if len(ogList.Items) == 0 { + return []CheckResult{{ + Name: "OperatorGroup exists", + Passed: false, + Message: fmt.Sprintf("no OperatorGroup found in namespace %s", opts.SubscriptionNamespace), + }}, nil + } + + og := ogList.Items[0] + var checks []CheckResult + + // spec.serviceAccountName (C6) + if og.Spec.ServiceAccountName != "" { + checks = append(checks, CheckResult{ + Name: "No scoped ServiceAccount", + Passed: false, + Message: "OperatorGroup has spec.serviceAccountName set; OLMv1 does not support scoped service accounts", + }) + } else { + checks = append(checks, CheckResult{ + Name: "No scoped ServiceAccount", + Passed: true, + Message: "OperatorGroup does not use a scoped service account", + }) + } + + // spec.selector + if og.Spec.Selector != nil && !isEmptyLabelSelector(og.Spec.Selector) { + checks = append(checks, CheckResult{ + Name: "No namespace selector", + Passed: false, + Message: "OperatorGroup has spec.selector set; must convert to spec.targetNamespaces before migration", + }) + } else { + checks = append(checks, CheckResult{ + Name: "No namespace selector", + Passed: true, + Message: "OperatorGroup does not use a namespace selector", + }) + } + + // spec.upgradeStrategy + if og.Spec.UpgradeStrategy != "" && og.Spec.UpgradeStrategy != operatorsv1.UpgradeStrategyDefault { + checks = append(checks, CheckResult{ + Name: "Upgrade strategy", + Passed: false, + Message: fmt.Sprintf("must be %q or unset, got %q", operatorsv1.UpgradeStrategyDefault, og.Spec.UpgradeStrategy), + }) + } else { + checks = append(checks, CheckResult{ + Name: "Upgrade strategy", + Passed: true, + Message: "upgrade strategy is Default or unset", + }) + } + + // spec.targetNamespaces — AllNamespaces mode (C1) + if len(og.Spec.TargetNamespaces) > 0 { + checks = append(checks, CheckResult{ + Name: "AllNamespaces mode", + Passed: false, + Message: "OperatorGroup has spec.targetNamespaces set; operator must be in AllNamespaces mode for migration", + }) + } else { + checks = append(checks, CheckResult{ + Name: "AllNamespaces mode", + Passed: true, + Message: "operator is in AllNamespaces mode", + }) + } + + // status.namespaces warning — single-namespace targets will become AllNamespaces + if len(og.Status.Namespaces) == 1 && og.Status.Namespaces[0] != "" { + checks = append(checks, CheckResult{ + Name: "Namespace scope change", + Passed: false, + Message: fmt.Sprintf("OperatorGroup targets namespace %q; post-migration the operator will run in AllNamespaces mode", og.Status.Namespaces[0]), + }) + } + + return checks, nil +} + +func isEmptyLabelSelector(s *metav1.LabelSelector) bool { + return s == nil || (len(s.MatchLabels) == 0 && len(s.MatchExpressions) == 0) +} + +// olmProperty represents a single entry in the operatorframework.io/properties annotation. +type olmProperty struct { + Type string `json:"type"` + Value json.RawMessage `json:"value"` +} + +// parseProperties handles both bare-array and wrapped-object formats of the +// operatorframework.io/properties annotation. +func parseProperties(propertiesJSON string) ([]olmProperty, error) { + raw := []byte(propertiesJSON) + + var props []olmProperty + if err := json.Unmarshal(raw, &props); err == nil { + return props, nil + } + + var wrapped struct { + Properties []olmProperty `json:"properties"` + } + if err := json.Unmarshal(raw, &wrapped); err != nil { + return nil, err + } + return wrapped.Properties, nil +} + +// checkNoDependencies enforces C2 — no olm.package.required or olm.gvk.required (hard block). +func checkNoDependencies(propertiesJSON string) []CheckResult { + if propertiesJSON == "" { + return []CheckResult{{ + Name: "No dependency resolution", + Passed: true, + Message: "no bundle properties declared", + }} + } + + props, err := parseProperties(propertiesJSON) + if err != nil { + return []CheckResult{{ + Name: "No dependency resolution", + Passed: false, + Message: fmt.Sprintf("failed to parse bundle properties: %v", err), + }} + } + + var issues []CheckResult + for _, p := range props { + switch p.Type { + case "olm.package.required": + issues = append(issues, CheckResult{ + Name: "No dependency resolution", + Passed: false, + Message: fmt.Sprintf("bundle declares olm.package.required dependency: %s", string(p.Value)), + }) + case "olm.gvk.required": + issues = append(issues, CheckResult{ + Name: "No dependency resolution", + Passed: false, + Message: fmt.Sprintf("bundle declares olm.gvk.required dependency: %s", string(p.Value)), + }) + } + } + + if len(issues) == 0 { + return []CheckResult{{ + Name: "No dependency resolution", + Passed: true, + Message: "no olm.package.required or olm.gvk.required properties", + }} + } + return issues +} + +// checkNoAPIServices enforces C3 — no APIService definitions (hard block, temporary until OPRUN-4723). +func checkNoAPIServices(csv *operatorsv1alpha1.ClusterServiceVersion) CheckResult { + if len(csv.Spec.APIServiceDefinitions.Owned) > 0 || len(csv.Spec.APIServiceDefinitions.Required) > 0 { + return CheckResult{ + Name: "No APIService definitions", + Passed: false, + Message: "CSV has spec.apiservicedefinitions set; OLMv1 does not yet support APIService definitions (tracked by OPRUN-4723)", + } + } + return CheckResult{ + Name: "No APIService definitions", + Passed: true, + Message: "CSV does not define APIServices", + } +} + +// checkNoOperatorConditions enforces C4 — no active OperatorCondition status entries. +// RBAC presence alone is NOT treated as usage; only status.conditions entries count. +func (m *Migrator) checkNoOperatorConditions(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) (CheckResult, error) { + var oc operatorsv1.OperatorCondition + err := m.Client.Get(ctx, types.NamespacedName{ + Name: csv.Name, + Namespace: opts.SubscriptionNamespace, + }, &oc) + if err != nil { + if client.IgnoreNotFound(err) != nil { + return CheckResult{}, fmt.Errorf("failed to get OperatorCondition: %w", err) + } + return CheckResult{ + Name: "No OperatorCondition usage", + Passed: true, + Message: "no OperatorCondition resource found", + }, nil + } + + if len(oc.Status.Conditions) > 0 { + return CheckResult{ + Name: "No OperatorCondition usage", + Passed: false, + Message: "OperatorCondition has status.conditions entries; operator actively uses the OperatorCondition API", + }, nil + } + return CheckResult{ + Name: "No OperatorCondition usage", + Passed: true, + Message: "OperatorCondition exists but has no status entries", + }, nil +} diff --git a/migration/pkg/migration/labels.go b/migration/pkg/migration/labels.go new file mode 100644 index 0000000..0d379bd --- /dev/null +++ b/migration/pkg/migration/labels.go @@ -0,0 +1,36 @@ +package migration + +// Label and annotation keys used by the migration tool. +// These match the values expected by operator-controller but are defined here +// to avoid importing internal packages from that module. +const ( + // LabelOwnerKind is set on ClusterObjectSet to indicate its owner's kind. + LabelOwnerKind = "olm.operatorframework.io/owner-kind" + // LabelOwnerName is set on ClusterObjectSet to indicate its owner's name. + LabelOwnerName = "olm.operatorframework.io/owner-name" + // LabelRevisionName is set on ref Secrets to identify the ClusterObjectSet. + LabelRevisionName = "olm.operatorframework.io/revision-name" + // LabelPackageName records the operator package associated with a ClusterObjectSet. + LabelPackageName = "olm.operatorframework.io/package-name" + // LabelBundleName records the bundle name for a ClusterObjectSet. + LabelBundleName = "olm.operatorframework.io/bundle-name" + // LabelBundleVersion records the bundle version for a ClusterObjectSet. + LabelBundleVersion = "olm.operatorframework.io/bundle-version" + // LabelBundleReference records the bundle image reference for a ClusterObjectSet. + LabelBundleReference = "olm.operatorframework.io/bundle-reference" + // LabelMetadataName is the well-known label key for ClusterCatalog name selection. + LabelMetadataName = "olm.operatorframework.io/metadata.name" + + // SecretTypeObjectData is the Secret type for externalized COS object content. + SecretTypeObjectData = "olm.operatorframework.io/object-data" + + // MigratedFromSubscriptionAnnotation is set on both the COS and CE. + // Value is "/" of the source Subscription. + MigratedFromSubscriptionAnnotation = "olm.operatorframework.io/migrated-from-subscription" + + // MigratedFromCatalogSourceAnnotation is set on ClusterCatalog by the catalog migration tool. + MigratedFromCatalogSourceAnnotation = "olm.operatorframework.io/migrated-from-catalogsource" + + // fieldManager is the SSA field manager used for all apply operations. + fieldManager = "olm.operatorframework.io/migration" +) diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go new file mode 100644 index 0000000..fc7d525 --- /dev/null +++ b/migration/pkg/migration/migration.go @@ -0,0 +1,689 @@ +package migration + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" + ocv1ac "github.com/operator-framework/operator-controller/applyconfigurations/api/v1" +) + +// annotationPrefixesToStrip are annotation prefixes that should be removed from migrated resources. +var annotationPrefixesToStrip = []string{ + "kubectl.kubernetes.io/", + "olm.operatorframework.io/installed-alongside", + "deployment.kubernetes.io/", +} + +// Migrate performs the full migration of an OLMv0-managed operator to OLMv1. +// Steps: +// 1. Profile the Operator (Subscription/CSV/InstallPlan) +// 2. Determine Compatibility and Readiness +// 3. Determine Target ClusterCatalog +// 4. Backup resources +// 5. Prepare for Migration (delete Sub/CSV with orphan cascade) +// 6. Collect Operator Resources +// 7. Create ClusterObjectSet (wait Succeeded=True) +// 8. Create ClusterExtension (wait Installed=True) +// 9. Clean Up OLMv0 Resources +func (m *Migrator) Migrate(ctx context.Context, opts Options) error { + opts.ApplyDefaults() + + _, csv, ip, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + return fmt.Errorf("failed to profile operator: %w", err) + } + + info, err := m.GetBundleInfo(ctx, opts, csv, ip) + if err != nil { + return fmt.Errorf("failed to get bundle info: %w", err) + } + + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + return fmt.Errorf("readiness check failed: %w", err) + } + if !readiness.Passed() { + return fmt.Errorf("readiness checks failed (%d issues)", len(readiness.FailedChecks())) + } + + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if err != nil { + return fmt.Errorf("compatibility check failed: %w", err) + } + if !compat.Passed() { + return fmt.Errorf("operator is not compatible with OLMv1 migration (%d issues found)", len(compat.FailedChecks())) + } + + catalogName, err := m.ResolveClusterCatalog(ctx, info, m.RESTConfig) + if err != nil { + return fmt.Errorf("failed to resolve ClusterCatalog: %w", err) + } + info.ResolvedCatalogName = catalogName + + backup, err := m.BackupResources(ctx, opts, csv) + if err != nil { + return fmt.Errorf("failed to backup resources: %w", err) + } + + if err := m.PrepareForMigration(ctx, opts, csv); err != nil { + if recoverErr := m.RecoverFromBackup(ctx, opts, backup); recoverErr != nil { + return fmt.Errorf("preparation failed: %w; recovery also failed: %v", err, recoverErr) + } + return fmt.Errorf("preparation failed (recovered): %w", err) + } + + objects, err := m.CollectResources(ctx, opts, csv, ip, info.PackageName) + if err != nil { + return fmt.Errorf("failed to collect resources: %w", err) + } + info.CollectedObjects = objects + + if err := m.CreateClusterObjectSet(ctx, opts, info); err != nil { + if recoverErr := m.RecoverBeforeCE(ctx, opts, backup); recoverErr != nil { + return fmt.Errorf("COS creation failed: %w; recovery also failed: %v", err, recoverErr) + } + return fmt.Errorf("COS creation failed (recovered): %w", err) + } + + if err := m.CreateClusterExtension(ctx, opts, info); err != nil { + return fmt.Errorf("failed to create ClusterExtension: %w", err) + } + + m.CleanupOLMv0Resources(ctx, opts, info.PackageName, csv.Name) + + return nil +} + +// EnsurePrerequisites verifies that all prerequisites for migration are met. +func (m *Migrator) EnsurePrerequisites(ctx context.Context, opts Options) (*operatorsv1alpha1.ClusterServiceVersion, *operatorsv1alpha1.InstallPlan, *PreMigrationReport, *PreMigrationReport, error) { + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + return nil, nil, nil, nil, err + } + + _, csv, ip, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + return nil, nil, nil, nil, err + } + + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if err != nil { + return nil, nil, nil, nil, err + } + + return csv, ip, readiness, compat, nil +} + +// BackupResources creates in-memory backup copies of the Subscription and CSV for recovery. +func (m *Migrator) BackupResources(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) (*Backup, error) { + var sub operatorsv1alpha1.Subscription + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: opts.SubscriptionName, + Namespace: opts.SubscriptionNamespace, + }, &sub); err != nil { + return nil, fmt.Errorf("failed to backup Subscription: %w", err) + } + + return &Backup{ + Subscription: sub.DeepCopy(), + ClusterServiceVersion: csv.DeepCopy(), + }, nil +} + +// PrepareForMigration removes OLMv0 management of the operator by deleting +// the Subscription and CSV with orphan cascading (operator workloads keep running). +func (m *Migrator) PrepareForMigration(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) error { + // Delete Subscription with orphan cascading + sub := &operatorsv1alpha1.Subscription{} + sub.Name = opts.SubscriptionName + sub.Namespace = opts.SubscriptionNamespace + if err := m.Client.Delete(ctx, sub, client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete Subscription: %w", err) + } + } + + // Delete CSV with orphan cascading + if err := m.Client.Delete(ctx, csv, client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete CSV: %w", err) + } + } + + return nil +} + +// RecoverFromBackup restores the Subscription from backup after a failed preparation. +func (m *Migrator) RecoverFromBackup(ctx context.Context, opts Options, backup *Backup) error { + if backup == nil { + return fmt.Errorf("no backup available for recovery") + } + + sub := backup.Subscription.DeepCopy() + sub.ResourceVersion = "" + sub.UID = "" + sub.Generation = 0 + sub.CreationTimestamp = metav1.Time{} + sub.Status = operatorsv1alpha1.SubscriptionStatus{} + + if backup.Subscription.Status.InstalledCSV != "" { + sub.Spec.StartingCSV = backup.Subscription.Status.InstalledCSV + } + + if err := m.Client.Create(ctx, sub); err != nil { + return fmt.Errorf("failed to re-create Subscription: %w", err) + } + + return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + var restored operatorsv1alpha1.Subscription + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: opts.SubscriptionName, + Namespace: opts.SubscriptionNamespace, + }, &restored); err != nil { + return false, err + } + if restored.Status.State == operatorsv1alpha1.SubscriptionStateAtLatest || + restored.Status.State == operatorsv1alpha1.SubscriptionStateUpgradePending { + return true, nil + } + m.progress(fmt.Sprintf("Subscription state: %s (waiting for AtLatestKnown)", restored.Status.State)) + return false, nil + }) +} + +// RecoverBeforeCE implements recovery when COS creation fails. +// Deletes the failed COS with orphan cascade, then restores the Subscription. +func (m *Migrator) RecoverBeforeCE(ctx context.Context, opts Options, backup *Backup) error { + cosName := fmt.Sprintf("%s-1", opts.ClusterExtensionName) + cos := &ocv1.ClusterObjectSet{} + cos.Name = cosName + if err := m.Client.Delete(ctx, cos, client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete COS during recovery: %w", err) + } + } + + return m.RecoverFromBackup(ctx, opts, backup) +} + +// CreateClusterObjectSet builds and creates a COS from the collected resources. +// It uses CollisionProtection=IfNoController so OLMv1 can adopt existing resources (including CRDs). +// The COS is annotated with the source Subscription reference. +func (m *Migrator) CreateClusterObjectSet(ctx context.Context, opts Options, info *MigrationInfo) error { + cosName := fmt.Sprintf("%s-1", opts.ClusterExtensionName) + + cosObjects := make([]ocv1ac.ClusterObjectSetObjectApplyConfiguration, 0, len(info.CollectedObjects)) + for _, obj := range info.CollectedObjects { + stripped := stripResource(obj) + cosObjects = append(cosObjects, *ocv1ac.ClusterObjectSetObject(). + WithObject(stripped). + WithCollisionProtection(ocv1.CollisionProtectionIfNoController)) + } + + phases := PhaseSort(cosObjects) + + cosSpec := ocv1ac.ClusterObjectSetSpec(). + WithRevision(1). + WithCollisionProtection(ocv1.CollisionProtectionIfNoController). + WithLifecycleState(ocv1.ClusterObjectSetLifecycleStateActive). + WithPhases(phases...) + + cosAnnotations := map[string]string{ + MigratedFromSubscriptionAnnotation: fmt.Sprintf("%s/%s", opts.SubscriptionNamespace, opts.SubscriptionName), + LabelPackageName: info.PackageName, + LabelBundleName: info.BundleName, + LabelBundleVersion: info.Version, + } + if info.BundleImage != "" { + cosAnnotations[LabelBundleReference] = info.BundleImage + } + + cos := ocv1ac.ClusterObjectSet(cosName). + WithSpec(cosSpec). + WithLabels(map[string]string{ + LabelOwnerKind: ocv1.ClusterExtensionKind, + LabelOwnerName: opts.ClusterExtensionName, + }). + WithAnnotations(cosAnnotations) + + cosObj := &ocv1.ClusterObjectSet{} + cosObj.Name = cosName + + cosData, err := json.Marshal(cos) + if err != nil { + return fmt.Errorf("failed to marshal COS: %w", err) + } + + if err := m.Client.Patch(ctx, cosObj, client.RawPatch(types.ApplyPatchType, cosData), + client.ForceOwnership, client.FieldOwner(fieldManager)); err != nil { + return fmt.Errorf("failed to apply ClusterObjectSet: %w", err) + } + + return m.WaitForCOSSucceeded(ctx, cosName) +} + +// WaitForCOSSucceeded waits for the COS to reach Succeeded=True. +func (m *Migrator) WaitForCOSSucceeded(ctx context.Context, cosName string) error { + return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + var cos ocv1.ClusterObjectSet + if err := m.Client.Get(ctx, types.NamespacedName{Name: cosName}, &cos); err != nil { + m.progress(fmt.Sprintf("Waiting for COS %s (not found yet)", cosName)) + return false, err + } + + for _, c := range cos.Status.Conditions { + if c.Type == ocv1.ClusterObjectSetTypeSucceeded && c.Status == metav1.ConditionTrue { + return true, nil + } + if c.Type == ocv1.ClusterObjectSetTypeSucceeded && c.Reason == ocv1.ClusterObjectSetReasonBlocked { + return false, fmt.Errorf("ClusterObjectSet %s is blocked: %s", cosName, c.Message) + } + } + + m.progress(fmt.Sprintf("Waiting for ClusterObjectSet %s to reach Succeeded=True...", cosName)) + return false, nil + }) +} + +// CreateClusterExtension creates a CE that adopts the COS. +// ServiceAccount is NOT set (deprecated and ignored in OLMv1). +// Migration annotations are added to the CE for AlreadyMigrated/Conflict detection. +func (m *Migrator) CreateClusterExtension(ctx context.Context, opts Options, info *MigrationInfo) error { + ce := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: opts.ClusterExtensionName, + Annotations: map[string]string{ + MigratedFromSubscriptionAnnotation: fmt.Sprintf("%s/%s", opts.SubscriptionNamespace, opts.SubscriptionName), + }, + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: opts.InstallNamespace, + // ServiceAccount is deliberately not set — deprecated and ignored in OLMv1. + Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeCatalog, + Catalog: &ocv1.CatalogFilter{ + PackageName: info.PackageName, + }, + }, + }, + } + + // Version pinning: Manual approval → pin to installed version; Automatic → channel-based upgrades. + if info.ManualApproval { + ce.Spec.Source.Catalog.Version = info.Version + } + + if info.Channel != "" { + ce.Spec.Source.Catalog.Channels = []string{info.Channel} + } + + if info.ResolvedCatalogName != "" { + ce.Spec.Source.Catalog.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + LabelMetadataName: info.ResolvedCatalogName, + }, + } + } + + if err := m.Client.Create(ctx, ce); err != nil { + return fmt.Errorf("failed to create ClusterExtension: %w", err) + } + + return m.WaitForClusterExtensionInstalled(ctx, opts.ClusterExtensionName) +} + +// WaitForClusterExtensionInstalled waits for the CE to reach Installed=True. +func (m *Migrator) WaitForClusterExtensionInstalled(ctx context.Context, ceName string) error { + return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + var ce ocv1.ClusterExtension + if err := m.Client.Get(ctx, types.NamespacedName{Name: ceName}, &ce); err != nil { + m.progress(fmt.Sprintf("Waiting for CE %s (not found yet)", ceName)) + return false, err + } + + for _, c := range ce.Status.Conditions { + if c.Type == ocv1.TypeInstalled && c.Status == metav1.ConditionTrue { + return true, nil + } + } + + m.progress(fmt.Sprintf("Waiting for ClusterExtension %s to reach Installed=True...", ceName)) + return false, nil + }) +} + +// CleanupAction describes a single cleanup operation and its result. +type CleanupAction struct { + Description string + Succeeded bool + Skipped bool + Error error +} + +// CleanupResult holds the results of all cleanup operations. +type CleanupResult struct { + Actions []CleanupAction +} + +// CleanupOLMv0Resources removes remaining OLMv0 resources after migration. +func (m *Migrator) CleanupOLMv0Resources(ctx context.Context, opts Options, packageName, csvName string) *CleanupResult { + result := &CleanupResult{} + + // 1. Delete the Operator CR + operatorName := fmt.Sprintf("%s.%s", packageName, opts.SubscriptionNamespace) + err := m.deleteOperatorCR(ctx, packageName, opts.SubscriptionNamespace) + result.Actions = append(result.Actions, CleanupAction{ + Description: fmt.Sprintf("Delete Operator CR %s", operatorName), + Succeeded: err == nil, + Error: err, + }) + + // 2. Delete the OperatorCondition + if csvName != "" { + err = m.deleteOperatorCondition(ctx, csvName, opts.SubscriptionNamespace) + result.Actions = append(result.Actions, CleanupAction{ + Description: fmt.Sprintf("Delete OperatorCondition %s/%s", opts.SubscriptionNamespace, csvName), + Succeeded: err == nil, + Error: err, + }) + + // 3. Delete copied CSVs + copiedCount, err := m.deleteCopiedCSVs(ctx, csvName) + if copiedCount > 0 { + result.Actions = append(result.Actions, CleanupAction{ + Description: fmt.Sprintf("Delete %d copied CSV(s)", copiedCount), + Succeeded: err == nil, + Error: err, + }) + } else { + result.Actions = append(result.Actions, CleanupAction{ + Description: "Delete copied CSVs", + Skipped: true, + }) + } + } + + // 4. OperatorGroup cleanup + ogActions := m.cleanupOperatorGroup(ctx, opts) + result.Actions = append(result.Actions, ogActions...) + + return result +} + +func (m *Migrator) deleteCopiedCSVs(ctx context.Context, csvName string) (int, error) { + var csvList operatorsv1alpha1.ClusterServiceVersionList + if err := m.Client.List(ctx, &csvList, + client.MatchingLabels{ + "olm.managed": "true", + "olm.copiedFrom": csvName, + }, + ); err != nil { + return 0, err + } + + deleted := 0 + for i := range csvList.Items { + if err := m.Client.Delete(ctx, &csvList.Items[i], client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil { + if client.IgnoreNotFound(err) != nil { + return deleted, err + } + } + deleted++ + } + return deleted, nil +} + +func (m *Migrator) deleteOperatorCR(ctx context.Context, packageName, namespace string) error { + operatorName := fmt.Sprintf("%s.%s", packageName, namespace) + op := &operatorsv1.Operator{} + op.Name = operatorName + if err := m.Client.Delete(ctx, op); err != nil { + return client.IgnoreNotFound(err) + } + return nil +} + +func (m *Migrator) deleteOperatorCondition(ctx context.Context, csvName, namespace string) error { + oc := &operatorsv1.OperatorCondition{} + oc.Name = csvName + oc.Namespace = namespace + if err := m.Client.Delete(ctx, oc); err != nil { + return client.IgnoreNotFound(err) + } + return nil +} + +// cleanupOperatorGroup deletes the OperatorGroup if no other Subscriptions remain in the namespace. +func (m *Migrator) cleanupOperatorGroup(ctx context.Context, opts Options) []CleanupAction { + var actions []CleanupAction + + var subList operatorsv1alpha1.SubscriptionList + if err := m.Client.List(ctx, &subList, client.InNamespace(opts.SubscriptionNamespace)); err != nil { + actions = append(actions, CleanupAction{ + Description: "Check remaining Subscriptions", + Error: err, + }) + return actions + } + + if len(subList.Items) > 0 { + actions = append(actions, CleanupAction{ + Description: fmt.Sprintf("Delete OperatorGroup (skipped: %d Subscription(s) remain)", len(subList.Items)), + Skipped: true, + }) + return actions + } + + var ogList operatorsv1.OperatorGroupList + if err := m.Client.List(ctx, &ogList, client.InNamespace(opts.SubscriptionNamespace)); err != nil { + actions = append(actions, CleanupAction{ + Description: "List OperatorGroups", + Error: err, + }) + return actions + } + + for i := range ogList.Items { + og := &ogList.Items[i] + + stripped := m.stripOGAggregationClusterRoles(ctx, og.Name) + for _, name := range stripped { + actions = append(actions, CleanupAction{ + Description: fmt.Sprintf("Strip OLM labels from aggregation ClusterRole %s", name), + Succeeded: true, + }) + } + + err := m.Client.Delete(ctx, og) + if err != nil && client.IgnoreNotFound(err) != nil { + actions = append(actions, CleanupAction{ + Description: fmt.Sprintf("Delete OperatorGroup %s/%s", og.Namespace, og.Name), + Error: err, + }) + } else { + actions = append(actions, CleanupAction{ + Description: fmt.Sprintf("Delete OperatorGroup %s/%s", og.Namespace, og.Name), + Succeeded: true, + }) + } + } + + return actions +} + +// stripOGAggregationClusterRoles strips olm.owner and olm.managed labels from +// OperatorGroup aggregation ClusterRoles (olm.og..-). +func (m *Migrator) stripOGAggregationClusterRoles(ctx context.Context, ogName string) []string { + prefix := fmt.Sprintf("olm.og.%s.", ogName) + + var crList unstructured.UnstructuredList + crList.SetAPIVersion("rbac.authorization.k8s.io/v1") + crList.SetKind("ClusterRoleList") + + if err := m.Client.List(ctx, &crList); err != nil { + return nil + } + + var stripped []string + for _, cr := range crList.Items { + if !strings.HasPrefix(cr.GetName(), prefix) { + continue + } + + lbls := cr.GetLabels() + if lbls == nil { + continue + } + + changed := false + for _, key := range []string{"olm.owner", "olm.owner.namespace", "olm.owner.kind", "olm.managed"} { + if _, ok := lbls[key]; ok { + delete(lbls, key) + changed = true + } + } + + if changed { + cr.SetLabels(lbls) + if err := m.Client.Update(ctx, &cr); err == nil { + stripped = append(stripped, cr.GetName()) + } + } + } + return stripped +} + +// FindCRDClusterRoles returns CRD-owned ClusterRoles that are not managed by OLMv1. +func (m *Migrator) FindCRDClusterRoles(ctx context.Context, csvName string) []string { + var crList unstructured.UnstructuredList + crList.SetAPIVersion("rbac.authorization.k8s.io/v1") + crList.SetKind("ClusterRoleList") + + if err := m.Client.List(ctx, &crList); err != nil { + return nil + } + + var crdRoles []string + for _, cr := range crList.Items { + name := cr.GetName() + lbls := cr.GetLabels() + if lbls != nil && lbls["olm.owner"] == csvName { + for _, suffix := range []string{"-admin", "-edit", "-view", "-crd"} { + if strings.HasSuffix(name, suffix) { + crdRoles = append(crdRoles, name) + break + } + } + } + } + return crdRoles +} + +// stripResource removes server-side fields from a resource for inclusion in a COS. +func stripResource(obj unstructured.Unstructured) unstructured.Unstructured { + stripped := unstructured.Unstructured{Object: make(map[string]interface{})} + + stripped.SetAPIVersion(obj.GetAPIVersion()) + stripped.SetKind(obj.GetKind()) + stripped.SetName(obj.GetName()) + if obj.GetNamespace() != "" { + stripped.SetNamespace(obj.GetNamespace()) + } + + if lbls := obj.GetLabels(); len(lbls) > 0 { + stripped.SetLabels(lbls) + } + + if annotations := obj.GetAnnotations(); len(annotations) > 0 { + filtered := filterAnnotations(annotations) + if len(filtered) > 0 { + stripped.SetAnnotations(filtered) + } + } + + if spec, ok := obj.Object["spec"]; ok { + stripped.Object["spec"] = spec + stripNestedAnnotations(&stripped) + } + + if data, ok := obj.Object["data"]; ok { + stripped.Object["data"] = data + } + if stringData, ok := obj.Object["stringData"]; ok { + stripped.Object["stringData"] = stringData + } + + if rules, ok := obj.Object["rules"]; ok { + stripped.Object["rules"] = rules + } + + if roleRef, ok := obj.Object["roleRef"]; ok { + stripped.Object["roleRef"] = roleRef + } + if subjects, ok := obj.Object["subjects"]; ok { + stripped.Object["subjects"] = subjects + } + + if webhooks, ok := obj.Object["webhooks"]; ok { + stripped.Object["webhooks"] = webhooks + } + + return stripped +} + +// filterAnnotations removes annotation prefixes that should not be migrated. +func filterAnnotations(annotations map[string]string) map[string]string { + filtered := make(map[string]string) + for k, v := range annotations { + shouldStrip := false + for _, prefix := range annotationPrefixesToStrip { + if strings.HasPrefix(k, prefix) { + shouldStrip = true + break + } + } + if !shouldStrip { + filtered[k] = v + } + } + return filtered +} + +// stripNestedAnnotations removes transient annotations from Deployment pod template metadata. +func stripNestedAnnotations(obj *unstructured.Unstructured) { + templateAnnotations, found, _ := unstructured.NestedMap(obj.Object, "spec", "template", "metadata", "annotations") + if found && templateAnnotations != nil { + filtered := make(map[string]interface{}) + for k, v := range templateAnnotations { + shouldStrip := false + for _, prefix := range annotationPrefixesToStrip { + if strings.HasPrefix(k, prefix) { + shouldStrip = true + break + } + } + if !shouldStrip { + filtered[k] = v + } + } + if len(filtered) > 0 { + _ = unstructured.SetNestedField(obj.Object, filtered, "spec", "template", "metadata", "annotations") + } else { + unstructured.RemoveNestedField(obj.Object, "spec", "template", "metadata", "annotations") + } + } +} diff --git a/migration/pkg/migration/phase.go b/migration/pkg/migration/phase.go new file mode 100644 index 0000000..291e2c2 --- /dev/null +++ b/migration/pkg/migration/phase.go @@ -0,0 +1,195 @@ +package migration + +// PhaseSort logic is adapted from: +// https://github.com/operator-framework/operator-controller/blob/main/internal/operator-controller/applier/phase.go +// which in turn is adapted from: +// https://github.com/package-operator/package-operator/blob/v1.18.2/internal/packages/internal/packagekickstart/presets/phases.go + +import ( + "cmp" + "slices" + + "k8s.io/apimachinery/pkg/runtime/schema" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + ocv1ac "github.com/operator-framework/operator-controller/applyconfigurations/api/v1" +) + +// Phase represents a well-known deployment phase name. +type Phase string + +const ( + PhaseNamespaces Phase = "namespaces" + PhasePolicies Phase = "policies" + PhaseIdentity Phase = "identity" + PhaseConfiguration Phase = "configuration" + PhaseStorage Phase = "storage" + PhaseCRDs Phase = "crds" + PhaseRoles Phase = "roles" + PhaseBindings Phase = "bindings" + PhaseInfrastructure Phase = "infrastructure" + PhaseDeploy Phase = "deploy" + PhaseScaling Phase = "scaling" + PhasePublish Phase = "publish" + PhaseAdmission Phase = "admission" +) + +// defaultPhaseOrder is the ordered list of phases for rollout sequencing. +var defaultPhaseOrder = []Phase{ + PhaseNamespaces, + PhasePolicies, + PhaseIdentity, + PhaseConfiguration, + PhaseStorage, + PhaseCRDs, + PhaseRoles, + PhaseBindings, + PhaseInfrastructure, + PhaseDeploy, + PhaseScaling, + PhasePublish, + PhaseAdmission, +} + +var ( + gkPhaseMap = map[schema.GroupKind]Phase{} + phaseGKMap = map[Phase][]schema.GroupKind{ + PhaseNamespaces: { + {Kind: "Namespace"}, + }, + PhasePolicies: { + {Kind: "NetworkPolicy", Group: "networking.k8s.io"}, + {Kind: "PodDisruptionBudget", Group: "policy"}, + {Kind: "PriorityClass", Group: "scheduling.k8s.io"}, + }, + PhaseIdentity: { + {Kind: "ServiceAccount"}, + }, + PhaseConfiguration: { + {Kind: "Secret"}, + {Kind: "ConfigMap"}, + }, + PhaseStorage: { + {Kind: "PersistentVolume"}, + {Kind: "PersistentVolumeClaim"}, + {Kind: "StorageClass", Group: "storage.k8s.io"}, + }, + PhaseCRDs: { + {Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io"}, + }, + PhaseRoles: { + {Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"}, + {Kind: "Role", Group: "rbac.authorization.k8s.io"}, + }, + PhaseBindings: { + {Kind: "ClusterRoleBinding", Group: "rbac.authorization.k8s.io"}, + {Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"}, + }, + PhaseInfrastructure: { + {Kind: "Service"}, + {Kind: "Issuer", Group: "cert-manager.io"}, + {Kind: "Certificate", Group: "cert-manager.io"}, + }, + PhaseDeploy: { + {Kind: "Deployment", Group: "apps"}, + }, + PhaseScaling: { + {Kind: "VerticalPodAutoscaler", Group: "autoscaling.k8s.io"}, + }, + PhasePublish: { + {Kind: "PrometheusRule", Group: "monitoring.coreos.com"}, + {Kind: "ServiceMonitor", Group: "monitoring.coreos.com"}, + {Kind: "PodMonitor", Group: "monitoring.coreos.com"}, + {Kind: "Ingress", Group: "networking.k8s.io"}, + {Kind: "Route", Group: "route.openshift.io"}, + {Kind: "ConsoleYAMLSample", Group: "console.openshift.io"}, + {Kind: "ConsoleQuickStart", Group: "console.openshift.io"}, + {Kind: "ConsoleCLIDownload", Group: "console.openshift.io"}, + {Kind: "ConsoleLink", Group: "console.openshift.io"}, + {Kind: "ConsolePlugin", Group: "console.openshift.io"}, + }, + PhaseAdmission: { + {Kind: "ValidatingWebhookConfiguration", Group: "admissionregistration.k8s.io"}, + {Kind: "MutatingWebhookConfiguration", Group: "admissionregistration.k8s.io"}, + }, + } +) + +func init() { + for phase, gks := range phaseGKMap { + for _, gk := range gks { + gkPhaseMap[gk] = phase + } + } +} + +func determinePhase(gk schema.GroupKind) Phase { + phase, ok := gkPhaseMap[gk] + if !ok { + return PhaseDeploy + } + return phase +} + +func compareObjects(a, b ocv1ac.ClusterObjectSetObjectApplyConfiguration) int { + var aGVK, bGVK schema.GroupVersionKind + if a.Object != nil { + aGVK = a.Object.GroupVersionKind() + } + if b.Object != nil { + bGVK = b.Object.GroupVersionKind() + } + var aNs, bNs, aName, bName string + if a.Object != nil { + aNs = a.Object.GetNamespace() + aName = a.Object.GetName() + } + if b.Object != nil { + bNs = b.Object.GetNamespace() + bName = b.Object.GetName() + } + return cmp.Or( + cmp.Compare(aGVK.Group, bGVK.Group), + cmp.Compare(aGVK.Version, bGVK.Version), + cmp.Compare(aGVK.Kind, bGVK.Kind), + cmp.Compare(aNs, bNs), + cmp.Compare(aName, bName), + ) +} + +// PhaseSort takes an unsorted list of objects and organizes them into sorted phases +// for use in a ClusterObjectSet spec. +func PhaseSort(unsortedObjs []ocv1ac.ClusterObjectSetObjectApplyConfiguration) []*ocv1ac.ClusterObjectSetPhaseApplyConfiguration { + phaseMap := make(map[Phase][]ocv1ac.ClusterObjectSetObjectApplyConfiguration) + + for _, obj := range unsortedObjs { + var gk schema.GroupKind + if obj.Object != nil { + gk = obj.Object.GroupVersionKind().GroupKind() + } + phase := determinePhase(gk) + phaseMap[phase] = append(phaseMap[phase], obj) + } + + var phasesSorted []*ocv1ac.ClusterObjectSetPhaseApplyConfiguration + for _, phaseName := range defaultPhaseOrder { + objs, ok := phaseMap[phaseName] + if !ok { + continue + } + slices.SortFunc(objs, compareObjects) + + objPtrs := make([]*ocv1ac.ClusterObjectSetObjectApplyConfiguration, len(objs)) + for i := range objs { + objPtrs[i] = &objs[i] + } + + cp := ocv1.CollisionProtectionIfNoController + phasesSorted = append(phasesSorted, ocv1ac.ClusterObjectSetPhase(). + WithName(string(phaseName)). + WithCollisionProtection(cp). + WithObjects(objPtrs...)) + } + + return phasesSorted +} diff --git a/migration/pkg/migration/readiness.go b/migration/pkg/migration/readiness.go new file mode 100644 index 0000000..166781e --- /dev/null +++ b/migration/pkg/migration/readiness.go @@ -0,0 +1,134 @@ +package migration + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +// CheckReadiness verifies that the cluster is ready for migration. +// It checks Subscription state, CSV health, uniqueness, and dependency status. +func (m *Migrator) CheckReadiness(ctx context.Context, opts Options) (*PreMigrationReport, error) { + report := &PreMigrationReport{} + + var sub operatorsv1alpha1.Subscription + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: opts.SubscriptionName, + Namespace: opts.SubscriptionNamespace, + }, &sub); err != nil { + return nil, fmt.Errorf("failed to get Subscription %s/%s: %w", opts.SubscriptionNamespace, opts.SubscriptionName, err) + } + + // Subscription state + if sub.Status.State == operatorsv1alpha1.SubscriptionStateAtLatest || + sub.Status.State == operatorsv1alpha1.SubscriptionStateUpgradePending { + report.Checks = append(report.Checks, CheckResult{ + Name: "Subscription state", + Passed: true, + Message: fmt.Sprintf("state is %q", sub.Status.State), + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "Subscription state", + Passed: false, + Message: fmt.Sprintf("must be %q or %q, got %q", operatorsv1alpha1.SubscriptionStateAtLatest, operatorsv1alpha1.SubscriptionStateUpgradePending, sub.Status.State), + }) + } + + // installedCSV + if sub.Status.InstalledCSV != "" { + report.Checks = append(report.Checks, CheckResult{ + Name: "Installed CSV", + Passed: true, + Message: sub.Status.InstalledCSV, + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "Installed CSV", + Passed: false, + Message: "no installedCSV set", + }) + } + + // olm.generated-by — auto-generated dependency Subscriptions must not be individually migrated + if _, ok := sub.Annotations["olm.generated-by"]; ok { + report.Checks = append(report.Checks, CheckResult{ + Name: "Not a dependency", + Passed: false, + Message: "olm.generated-by annotation present — operator is an OLMv0-managed dependency of another operator; do not migrate individually", + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "Not a dependency", + Passed: true, + Message: "no olm.generated-by annotation", + }) + } + + // Uniqueness — no other Subscription should reference the same package + var subList operatorsv1alpha1.SubscriptionList + if err := m.Client.List(ctx, &subList); err != nil { + return nil, fmt.Errorf("failed to list Subscriptions: %w", err) + } + duplicate := false + for _, other := range subList.Items { + if other.Name == sub.Name && other.Namespace == sub.Namespace { + continue + } + if other.Spec.Package == sub.Spec.Package { + report.Checks = append(report.Checks, CheckResult{ + Name: "Package uniqueness", + Passed: false, + Message: fmt.Sprintf("another Subscription %s/%s references the same package %q", other.Namespace, other.Name, sub.Spec.Package), + }) + duplicate = true + break + } + } + if !duplicate { + report.Checks = append(report.Checks, CheckResult{ + Name: "Package uniqueness", + Passed: true, + Message: fmt.Sprintf("no other Subscription references package %q", sub.Spec.Package), + }) + } + + // CSV phase and reason + if sub.Status.InstalledCSV != "" { + csvName := sub.Status.InstalledCSV + var csv operatorsv1alpha1.ClusterServiceVersion + if err := m.Client.Get(ctx, types.NamespacedName{ + Name: csvName, + Namespace: opts.SubscriptionNamespace, + }, &csv); err != nil { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: false, + Message: fmt.Sprintf("failed to get CSV %s: %v", csvName, err), + }) + } else if csv.Status.Phase != operatorsv1alpha1.CSVPhaseSucceeded { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: false, + Message: fmt.Sprintf("phase is %q, expected %q", csv.Status.Phase, operatorsv1alpha1.CSVPhaseSucceeded), + }) + } else if csv.Status.Reason != operatorsv1alpha1.CSVReasonInstallSuccessful { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: false, + Message: fmt.Sprintf("reason is %q, expected %q", csv.Status.Reason, operatorsv1alpha1.CSVReasonInstallSuccessful), + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: true, + Message: fmt.Sprintf("phase: %s, reason: %s", csv.Status.Phase, csv.Status.Reason), + }) + } + } + + return report, nil +} diff --git a/migration/pkg/migration/scan.go b/migration/pkg/migration/scan.go new file mode 100644 index 0000000..8a40f26 --- /dev/null +++ b/migration/pkg/migration/scan.go @@ -0,0 +1,396 @@ +package migration + +import ( + "context" + "encoding/json" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// OperatorScanResult holds the result of scanning a single Subscription for migration eligibility. +type OperatorScanResult struct { + SubscriptionName string + SubscriptionNamespace string + PackageName string + InstalledCSV string + Version string + State string + Status OperatorStatus // four-state classification + Eligible bool // true when Status == Eligible (backwards compat) + Error error + FailedChecks []CheckResult +} + +// ScanAllSubscriptions discovers all Subscriptions on the cluster, checks each for migration +// eligibility, and also detects AlreadyMigrated and Conflict states from ClusterExtensions. +func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResult, error) { + // List all Subscriptions + var subList operatorsv1alpha1.SubscriptionList + if err := m.Client.List(ctx, &subList); err != nil { + return nil, fmt.Errorf("failed to list Subscriptions: %w", err) + } + + // List all ClusterExtensions with migrated-from-subscription annotation + var ceList ocv1.ClusterExtensionList + if err := m.Client.List(ctx, &ceList); err != nil { + return nil, fmt.Errorf("failed to list ClusterExtensions: %w", err) + } + + // Build a map of migration-annotated CEs: "/" -> CE name + migratedCEBySubRef := make(map[string]string) + for _, ce := range ceList.Items { + if ref, ok := ce.Annotations[MigratedFromSubscriptionAnnotation]; ok { + migratedCEBySubRef[ref] = ce.Name + } + } + + // Build a set of Subscription refs that currently exist + existingSubs := make(map[string]bool) + for _, sub := range subList.Items { + existingSubs[fmt.Sprintf("%s/%s", sub.Namespace, sub.Name)] = true + } + + var results []OperatorScanResult + + // Check each existing Subscription + for _, sub := range subList.Items { + subRef := fmt.Sprintf("%s/%s", sub.Namespace, sub.Name) + + result := OperatorScanResult{ + SubscriptionName: sub.Name, + SubscriptionNamespace: sub.Namespace, + PackageName: sub.Spec.Package, + InstalledCSV: sub.Status.InstalledCSV, + State: string(sub.Status.State), + } + + // Conflict: both Subscription and annotated CE exist + if _, hasCE := migratedCEBySubRef[subRef]; hasCE { + result.Status = OperatorStatusConflict + result.Eligible = false + result.Error = fmt.Errorf("both Subscription and annotated ClusterExtension exist; resolve with cleanup or rollback") + results = append(results, result) + continue + } + + opts := Options{ + SubscriptionName: sub.Name, + SubscriptionNamespace: sub.Namespace, + } + opts.ApplyDefaults() + + m.progress(fmt.Sprintf("Checking %s/%s (%s)...", sub.Namespace, sub.Name, sub.Spec.Package)) + + // Readiness checks + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + result.Status = OperatorStatusIneligible + result.Error = err + results = append(results, result) + continue + } + + // Get CSV for compatibility checks + _, csv, _, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + result.Status = OperatorStatusIneligible + result.Error = fmt.Errorf("failed to get CSV: %w", err) + results = append(results, result) + continue + } + + result.Version = parseCSVVersion(csv) + + // Compatibility checks + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if err != nil { + result.Status = OperatorStatusIneligible + result.Error = fmt.Errorf("compatibility check error: %w", err) + results = append(results, result) + continue + } + + // Merge failed checks + result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) + if len(result.FailedChecks) == 0 { + result.Status = OperatorStatusEligible + result.Eligible = true + } else { + result.Status = OperatorStatusIneligible + result.Eligible = false + } + results = append(results, result) + } + + // Check for AlreadyMigrated: CE with annotation but no matching Subscription + for subRef, ceName := range migratedCEBySubRef { + if existingSubs[subRef] { + continue // handled above as Conflict or normal sub + } + results = append(results, OperatorScanResult{ + SubscriptionName: ceName, + SubscriptionNamespace: "", + PackageName: "", + Status: OperatorStatusAlreadyMigrated, + Eligible: false, + State: fmt.Sprintf("ClusterExtension %s (migrated from %s)", ceName, subRef), + }) + } + + return results, nil +} + +// ScanSubscription checks a single Subscription and returns its scan result. +func (m *Migrator) ScanSubscription(ctx context.Context, opts Options) (*OperatorScanResult, error) { + opts.ApplyDefaults() + + result := &OperatorScanResult{ + SubscriptionName: opts.SubscriptionName, + SubscriptionNamespace: opts.SubscriptionNamespace, + } + + // Check for Conflict first + var ceList ocv1.ClusterExtensionList + if err := m.Client.List(ctx, &ceList); err != nil { + return nil, fmt.Errorf("failed to list ClusterExtensions: %w", err) + } + subRef := fmt.Sprintf("%s/%s", opts.SubscriptionNamespace, opts.SubscriptionName) + for _, ce := range ceList.Items { + if ref, ok := ce.Annotations[MigratedFromSubscriptionAnnotation]; ok && ref == subRef { + result.Status = OperatorStatusConflict + result.Error = fmt.Errorf("both Subscription and annotated ClusterExtension %s exist; resolve with cleanup or rollback", ce.Name) + return result, nil + } + } + + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + return nil, err + } + + _, csv, _, err := m.GetCSVAndInstallPlan(ctx, opts) + if err != nil { + result.Status = OperatorStatusIneligible + result.Error = err + return result, nil + } + + result.PackageName = csv.Spec.Description + result.InstalledCSV = csv.Name + result.Version = parseCSVVersion(csv) + + propsJSON := csv.Annotations["operatorframework.io/properties"] + compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) + if err != nil { + result.Status = OperatorStatusIneligible + result.Error = err + return result, nil + } + + result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) + if len(result.FailedChecks) == 0 { + result.Status = OperatorStatusEligible + result.Eligible = true + } else { + result.Status = OperatorStatusIneligible + } + return result, nil +} + +// PrintScanSummary prints results in the required order: Conflict → Ineligible → AlreadyMigrated → Eligible. +func PrintScanSummary(results []OperatorScanResult, printf func(string, ...interface{})) { + byStatus := make(map[OperatorStatus][]OperatorScanResult) + for _, r := range results { + byStatus[r.Status] = append(byStatus[r.Status], r) + } + + order := []OperatorStatus{ + OperatorStatusConflict, + OperatorStatusIneligible, + OperatorStatusAlreadyMigrated, + OperatorStatusEligible, + } + + for _, status := range order { + list := byStatus[status] + if len(list) == 0 { + continue + } + printf("\n=== %s (%d) ===\n", status, len(list)) + for _, r := range list { + switch status { + case OperatorStatusConflict: + printf(" ⚠️ %s/%s — CONFLICT: %v\n", r.SubscriptionNamespace, r.SubscriptionName, r.Error) + case OperatorStatusIneligible: + printf(" ✗ %s/%s", r.SubscriptionNamespace, r.SubscriptionName) + for _, fc := range r.FailedChecks { + printf("\n [%s] %s", fc.Name, fc.Message) + } + if r.Error != nil { + printf("\n error: %v", r.Error) + } + printf("\n") + case OperatorStatusAlreadyMigrated: + printf(" ✓ %s (already migrated)\n", r.State) + case OperatorStatusEligible: + printf(" ✓ %s/%s (%s)\n", r.SubscriptionNamespace, r.SubscriptionName, r.PackageName) + } + } + } +} + +// EligibleFromScan returns only the Eligible results from a scan. +func EligibleFromScan(results []OperatorScanResult) []OperatorScanResult { + var eligible []OperatorScanResult + for _, r := range results { + if r.Status == OperatorStatusEligible { + eligible = append(eligible, r) + } + } + return eligible +} + +// migrationAnnotatedCEsForSub returns ClusterExtension names that are annotated +// with the given subscription ref, or empty if none. +func migrationAnnotatedCEsForSub(ceList *ocv1.ClusterExtensionList, subRef string) []string { + var names []string + for _, ce := range ceList.Items { + if ref, ok := ce.Annotations[MigratedFromSubscriptionAnnotation]; ok && ref == subRef { + names = append(names, ce.Name) + } + } + return names +} + +// RollbackClusterExtension deletes the CE and COS (orphan cascade), then restores the Subscription. +func (m *Migrator) RollbackClusterExtension(ctx context.Context, ceName string, acknowledgeInstalled bool) error { + var ce ocv1.ClusterExtension + if err := m.Client.Get(ctx, client.ObjectKey{Name: ceName}, &ce); err != nil { + return fmt.Errorf("failed to get ClusterExtension %s: %w", ceName, err) + } + + // Check if Installed=True and require acknowledgment + if !acknowledgeInstalled { + for _, cond := range ce.Status.Conditions { + if cond.Type == "Installed" && cond.Status == "True" { + return fmt.Errorf("ClusterExtension %s is Installed=True; pass --acknowledge-installed to confirm rollback", ceName) + } + } + } + + // Delete CE (orphan cascade — preserves operator workloads) + if err := m.Client.Delete(ctx, &ce, client.PropagationPolicy("Orphan")); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete ClusterExtension: %w", err) + } + } + + // Delete COS (orphan cascade) + cosName := fmt.Sprintf("%s-1", ceName) + var cos ocv1.ClusterObjectSet + if err := m.Client.Get(ctx, client.ObjectKey{Name: cosName}, &cos); err == nil { + if err := m.Client.Delete(ctx, &cos, client.PropagationPolicy("Orphan")); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete ClusterObjectSet: %w", err) + } + } + } + + // Restore Subscription from backup annotation + subBackupJSON, ok := ce.Annotations["olm.operatorframework.io/migration-subscription-backup"] + if !ok || subBackupJSON == "" { + return fmt.Errorf("ClusterExtension %s has no migration-subscription-backup annotation; cannot restore Subscription", ceName) + } + + subRef := ce.Annotations[MigratedFromSubscriptionAnnotation] + if subRef == "" { + return fmt.Errorf("ClusterExtension %s has no migrated-from-subscription annotation", ceName) + } + + // Restore Subscription + var subSpec operatorsv1alpha1.SubscriptionSpec + if err := unmarshalJSON(subBackupJSON, &subSpec); err != nil { + return fmt.Errorf("failed to unmarshal subscription backup: %w", err) + } + + ns, name, err := splitNamespacedName(subRef) + if err != nil { + return fmt.Errorf("invalid migrated-from-subscription annotation %q: %w", subRef, err) + } + + restoredSub := &operatorsv1alpha1.Subscription{} + restoredSub.Name = name + restoredSub.Namespace = ns + restoredSub.Spec = &subSpec + + if err := m.Client.Create(ctx, restoredSub); err != nil { + return fmt.Errorf("failed to restore Subscription %s/%s: %w", ns, name, err) + } + + m.progress(fmt.Sprintf("Subscription %s/%s restored; operator returning to OLMv0 management", ns, name)) + return nil +} + +// CleanupConflict resolves a Conflict state: deletes the Subscription and OLMv0 artifacts, +// leaving the ClusterExtension intact. +func (m *Migrator) CleanupConflict(ctx context.Context, ceName string) error { + var ce ocv1.ClusterExtension + if err := m.Client.Get(ctx, client.ObjectKey{Name: ceName}, &ce); err != nil { + return fmt.Errorf("failed to get ClusterExtension %s: %w", ceName, err) + } + + subRef := ce.Annotations[MigratedFromSubscriptionAnnotation] + if subRef == "" { + return fmt.Errorf("ClusterExtension %s has no migrated-from-subscription annotation", ceName) + } + + ns, name, err := splitNamespacedName(subRef) + if err != nil { + return fmt.Errorf("invalid migrated-from-subscription annotation %q: %w", subRef, err) + } + + // Delete Subscription (orphan) + sub := &operatorsv1alpha1.Subscription{} + sub.Name = name + sub.Namespace = ns + if err := m.Client.Delete(ctx, sub, client.PropagationPolicy("Orphan")); err != nil { + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete Subscription %s/%s: %w", ns, name, err) + } + } + m.progress(fmt.Sprintf("Deleted Subscription %s/%s", ns, name)) + + // Cleanup remaining OLMv0 resources + opts := Options{ + SubscriptionName: name, + SubscriptionNamespace: ns, + ClusterExtensionName: ceName, + InstallNamespace: ns, + } + + // Try to get package name from CE annotations + packageName := ce.Spec.Source.Catalog.PackageName + csvName := "" // best effort + m.CleanupOLMv0Resources(ctx, opts, packageName, csvName) + + return nil +} + +// splitNamespacedName splits "namespace/name" into its components. +func splitNamespacedName(ref string) (string, string, error) { + for i, c := range ref { + if c == '/' { + return ref[:i], ref[i+1:], nil + } + } + return "", "", fmt.Errorf("expected namespace/name format, got %q", ref) +} + +func unmarshalJSON(data string, v interface{}) error { + return json.Unmarshal([]byte(data), v) +} diff --git a/migration/pkg/migration/types.go b/migration/pkg/migration/types.go new file mode 100644 index 0000000..292c502 --- /dev/null +++ b/migration/pkg/migration/types.go @@ -0,0 +1,79 @@ +package migration + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +// OperatorStatus is the four-state classification of a Subscription's migration readiness. +type OperatorStatus string + +const ( + OperatorStatusEligible OperatorStatus = "Eligible" + OperatorStatusIneligible OperatorStatus = "Ineligible" + OperatorStatusAlreadyMigrated OperatorStatus = "AlreadyMigrated" + OperatorStatusConflict OperatorStatus = "Conflict" +) + +// Options configures the migration process. +type Options struct { + SubscriptionName string + SubscriptionNamespace string + ClusterExtensionName string + InstallNamespace string +} + +// ApplyDefaults fills in default values for any unset optional fields. +func (o *Options) ApplyDefaults() { + if o.ClusterExtensionName == "" { + o.ClusterExtensionName = o.SubscriptionName + } + if o.InstallNamespace == "" { + o.InstallNamespace = o.SubscriptionNamespace + } +} + +// MigrationInfo holds the profiled operator information gathered during the migration. +type MigrationInfo struct { + PackageName string + Version string + BundleName string + BundleImage string + Channel string + ManualApproval bool // true if the Subscription had Manual install plan approval + CatalogSourceRef types.NamespacedName + CatalogSourceImage string // tag-based image from CatalogSource.Spec.Image + ResolvedCatalogName string + CollectedObjects []unstructured.Unstructured +} + +// ProgressFunc is called periodically during wait operations to report status. +type ProgressFunc func(message string) + +// Migrator performs the migration operations using a controller-runtime client. +type Migrator struct { + Client client.Client + RESTConfig *rest.Config + Progress ProgressFunc +} + +// NewMigrator creates a new Migrator with the given client and REST config. +func NewMigrator(c client.Client, cfg *rest.Config) *Migrator { + return &Migrator{Client: c, RESTConfig: cfg} +} + +func (m *Migrator) progress(msg string) { + if m.Progress != nil { + m.Progress(msg) + } +} + +// Backup holds serialized copies of resources for recovery. +type Backup struct { + Subscription *operatorsv1alpha1.Subscription + ClusterServiceVersion *operatorsv1alpha1.ClusterServiceVersion +} From 5b738b3a0a1e2d22da71f12c809b2dc02aba286c Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 21 Aug 2026 15:30:32 -0400 Subject: [PATCH 02/22] Add Makefile, bingo tooling, and golangci-lint config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makefile (build/test/lint/verify/api-diff targets): build / build-migrate-operators / build-migrate-catalogs → compile CLIs into bin/ build-all → go build ./... test / test-verbose → go test ./... fmt / vet / tidy → standard Go checks lint → golangci-lint via bingo verify → tidy + fmt + vet + lint + git diff check api-diff → go-apidiff against origin/main via bingo clean → remove bin/ artifacts .bingo/ pins golangci-lint v2.8.0 and go-apidiff v0.8.3 using bingo v0.9, matching operator-controller repo conventions. .golangci.yaml copied from operator-controller and adapted: - module prefix updated to library-olm for gci import ordering - operator-controller-internal alias replaced with migration package aliases .gitignore updated: binaries are produced in bin/ (already covered by /bin/); root-level CLI name patterns removed. Lint fixes to reach zero issues: - gci: import ordering auto-fixed in 6 files - gosec G101: nolint on SecretTypeObjectData (false positive) - nestif: nolint on --all CLI branches (inherently complex) - unused: removed checkSubscriptionName var, rollbackSingleCE stub, resource() output helper, defaultPollMinutes now wired in as poll-interval default, migrationAnnotatedCEsForSub inlined - unparam: removed unused restCfg from runConvertDryRun - staticcheck QF1008: embedded field selectors simplified Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- .bingo/.gitignore | 13 + .bingo/README.md | 14 + .bingo/Variables.mk | 31 + .bingo/go-apidiff.mod | 5 + .bingo/go-apidiff.sum | 86 ++ .bingo/go.mod | 1 + .bingo/golangci-lint.mod | 5 + .bingo/golangci-lint.sum | 949 ++++++++++++++++++ .bingo/variables.env | 14 + .golangci.yaml | 77 ++ Makefile | 81 ++ .../cmd/migrate-catalogs-v0-to-v1/main.go | 4 +- .../cmd/migrate-operators-v0-to-v1/check.go | 5 +- .../cmd/migrate-operators-v0-to-v1/cleanup.go | 7 +- .../cmd/migrate-operators-v0-to-v1/convert.go | 18 +- .../cmd/migrate-operators-v0-to-v1/output.go | 4 - .../migrate-operators-v0-to-v1/rollback.go | 19 +- .../pkg/catalogmigration/catalogmigration.go | 16 +- migration/pkg/migration/labels.go | 2 +- migration/pkg/migration/scan.go | 12 - 20 files changed, 1307 insertions(+), 56 deletions(-) create mode 100644 .bingo/.gitignore create mode 100644 .bingo/README.md create mode 100644 .bingo/Variables.mk create mode 100644 .bingo/go-apidiff.mod create mode 100644 .bingo/go-apidiff.sum create mode 100644 .bingo/go.mod create mode 100644 .bingo/golangci-lint.mod create mode 100644 .bingo/golangci-lint.sum create mode 100644 .bingo/variables.env create mode 100644 .golangci.yaml create mode 100644 Makefile diff --git a/.bingo/.gitignore b/.bingo/.gitignore new file mode 100644 index 0000000..9efccf6 --- /dev/null +++ b/.bingo/.gitignore @@ -0,0 +1,13 @@ + +# Ignore everything +* + +# But not these files: +!.gitignore +!*.mod +!*.sum +!README.md +!Variables.mk +!variables.env + +*tmp.mod diff --git a/.bingo/README.md b/.bingo/README.md new file mode 100644 index 0000000..7a5c2d4 --- /dev/null +++ b/.bingo/README.md @@ -0,0 +1,14 @@ +# Project Development Dependencies. + +This is directory which stores Go modules with pinned buildable package that is used within this repository, managed by https://github.com/bwplotka/bingo. + +* Run `bingo get` to install all tools having each own module file in this directory. +* Run `bingo get ` to install that have own module file in this directory. +* For Makefile: Make sure to put `include .bingo/Variables.mk` in your Makefile, then use $() variable where is the .bingo/.mod. +* For shell: Run `source .bingo/variables.env` to source all environment variable for each tool. +* For go: Import `.bingo/variables.go` to for variable names. +* See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies. + +## Requirements + +* Go 1.14+ diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk new file mode 100644 index 0000000..45b68f3 --- /dev/null +++ b/.bingo/Variables.mk @@ -0,0 +1,31 @@ +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. +# All tools are designed to be build inside $GOBIN. +BINGO_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +GOPATH ?= $(shell go env GOPATH) +GOBIN ?= $(firstword $(subst :, ,${GOPATH}))/bin +GO ?= $(shell which go) + +# Below generated variables ensure that every time a tool under each variable is invoked, the correct version +# will be used; reinstalling only if needed. +# For example for go-apidiff variable: +# +# In your main Makefile (for non array binaries): +# +#include .bingo/Variables.mk # Assuming -dir was set to .bingo . +# +#command: $(GO_APIDIFF) +# @echo "Running go-apidiff" +# @$(GO_APIDIFF) +# +GO_APIDIFF := $(GOBIN)/go-apidiff-v0.8.3 +$(GO_APIDIFF): $(BINGO_DIR)/go-apidiff.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)/go-apidiff-v0.8.3" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=go-apidiff.mod -o=$(GOBIN)/go-apidiff-v0.8.3 "github.com/joelanford/go-apidiff" + +GOLANGCI_LINT := $(GOBIN)/golangci-lint-v2.8.0 +$(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.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)/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" + diff --git a/.bingo/go-apidiff.mod b/.bingo/go-apidiff.mod new file mode 100644 index 0000000..bc7c87d --- /dev/null +++ b/.bingo/go-apidiff.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.26.5 + +require github.com/joelanford/go-apidiff v0.8.3 diff --git a/.bingo/go-apidiff.sum b/.bingo/go-apidiff.sum new file mode 100644 index 0000000..22bb731 --- /dev/null +++ b/.bingo/go-apidiff.sum @@ -0,0 +1,86 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= +github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/joelanford/go-apidiff v0.8.3 h1:pj3KnTX0VqH6AYk2AzUBB+hsANk10VMz5oCRRCvi/t4= +github.com/joelanford/go-apidiff v0.8.3/go.mod h1:V5YAvsIzCNB8POAR2y4NFjn3sKIRNSWktBCVO8hO/9s= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.bingo/go.mod b/.bingo/go.mod new file mode 100644 index 0000000..610249a --- /dev/null +++ b/.bingo/go.mod @@ -0,0 +1 @@ +module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files. \ No newline at end of file diff --git a/.bingo/golangci-lint.mod b/.bingo/golangci-lint.mod new file mode 100644 index 0000000..6a2e3a0 --- /dev/null +++ b/.bingo/golangci-lint.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.26.5 + +require github.com/golangci/golangci-lint/v2 v2.8.0 // cmd/golangci-lint diff --git a/.bingo/golangci-lint.sum b/.bingo/golangci-lint.sum new file mode 100644 index 0000000..cf6f305 --- /dev/null +++ b/.bingo/golangci-lint.sum @@ -0,0 +1,949 @@ +4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= +4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= +4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= +4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= +codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= +codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= +dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= +dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= +github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= +github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= +github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= +github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= +github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= +github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= +github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= +github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= +github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= +github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= +github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= +github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= +github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= +github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/MirrexOne/unqueryvet v1.4.0 h1:6KAkqqW2KUnkl9Z0VuTphC3IXRPoFqEkJEtyxxHj5eQ= +github.com/MirrexOne/unqueryvet v1.4.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= +github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= +github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= +github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= +github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= +github.com/alexkohler/prealloc v1.0.1 h1:A9P1haqowqUxWvU9nk6tQ7YktXIHf+LQM9wPRhuteEE= +github.com/alexkohler/prealloc v1.0.1/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= +github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= +github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= +github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= +github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= +github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= +github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= +github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= +github.com/bombsimon/wsl/v5 v5.3.0 h1:nZWREJFL6U3vgW/B1lfDOigl+tEF6qgs6dGGbFeR0UM= +github.com/bombsimon/wsl/v5 v5.3.0/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I= +github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= +github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= +github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= +github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= +github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= +github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= +github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= +github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= +github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= +github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= +github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= +github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= +github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= +github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= +github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= +github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= +github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= +github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/ghostiam/protogetter v0.3.18 h1:yEpghRGtP9PjKvVXtEzGpYfQj1Wl/ZehAfU6fr62Lfo= +github.com/ghostiam/protogetter v0.3.18/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godoc-lint/godoc-lint v0.11.1 h1:z9as8Qjiy6miRIa3VRymTa+Gt2RLnGICVikcvlUVOaA= +github.com/godoc-lint/godoc-lint v0.11.1/go.mod h1:BAqayheFSuZrEAqCRxgw9MyvsM+S/hZwJbU1s/ejRj8= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= +github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= +github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= +github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= +github.com/golangci/golangci-lint/v2 v2.8.0 h1:wJnr3hJWY3eVzOUcfwbDc2qbi2RDEpvLmQeNFaPSNYA= +github.com/golangci/golangci-lint/v2 v2.8.0/go.mod h1:xl+HafQ9xoP8rzw0z5AwnO5kynxtb80e8u02Ej/47RI= +github.com/golangci/golines v0.14.0 h1:xt9d3RKBjhasA3qpoXs99J2xN2t6eBlpLHt0TrgyyXc= +github.com/golangci/golines v0.14.0/go.mod h1:gf555vPG2Ia7mmy2mzmhVQbVjuK8Orw0maR1G4vVAAQ= +github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c= +github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= +github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= +github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= +github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= +github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= +github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= +github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= +github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= +github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= +github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= +github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= +github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= +github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= +github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= +github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= +github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= +github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= +github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= +github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= +github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= +github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= +github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= +github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= +github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= +github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= +github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= +github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= +github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= +github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= +github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= +github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= +github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= +github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= +github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= +github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= +github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= +github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= +github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= +github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= +github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= +github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= +github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgechev/revive v1.13.0 h1:yFbEVliCVKRXY8UgwEO7EOYNopvjb1BFbmYqm9hZjBM= +github.com/mgechev/revive v1.13.0/go.mod h1:efJfeBVCX2JUumNQ7dtOLDja+QKj9mYGgEZA7rt5u+0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.21.2 h1:khzWfm2/Br8ZemX8QM1pl72LwM+rMeW6VUbQ4rzh0Po= +github.com/nunnatsa/ginkgolinter v0.21.2/go.mod h1:GItSI5fw7mCGLPmkvGYrr1kEetZe7B593jcyOpyabsY= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= +github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= +github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= +github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= +github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= +github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= +github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= +github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= +github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg= +github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o= +github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= +github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= +github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= +github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= +github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= +github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= +github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= +github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= +github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= +github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= +github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= +github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= +github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= +go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= +go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= +go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= +go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= +go.augendre.info/arangolint v0.3.1 h1:n2E6p8f+zfXSFLa2e2WqFPp4bfvcuRdd50y6cT65pSo= +go.augendre.info/arangolint v0.3.1/go.mod h1:6ZKzEzIZuBQwoSvlKT+qpUfIbBfFCE5gbAoTg0/117g= +go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= +go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE= +golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= +mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= +mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/.bingo/variables.env b/.bingo/variables.env new file mode 100644 index 0000000..7fd3cdf --- /dev/null +++ b/.bingo/variables.env @@ -0,0 +1,14 @@ +# Auto generated binary variables helper managed by https://github.com/bwplotka/bingo v0.9. DO NOT EDIT. +# All tools are designed to be build inside $GOBIN. +# Those variables will work only until 'bingo get' was invoked, or if tools were installed via Makefile's Variables.mk. +GOBIN=${GOBIN:=$(go env GOBIN)} + +if [ -z "$GOBIN" ]; then + GOBIN="$(go env GOPATH)/bin" +fi + + +GO_APIDIFF="${GOBIN}/go-apidiff-v0.8.3" + +GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.8.0" + diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..f4dc772 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,77 @@ +version: "2" +output: + formats: + tab: + path: stdout + colors: false +linters: + enable: + - asciicheck + - bodyclose + - errorlint + - gosec + - importas + - misspell + - nestif + - nonamedreturns + - prealloc + - staticcheck + - testifylint + - tparallel + - unconvert + - unparam + - whitespace + settings: + errorlint: + errorf: false + importas: + alias: + - pkg: k8s.io/apimachinery/pkg/apis/meta/v1 + alias: metav1 + - pkg: k8s.io/apimachinery/pkg/api/errors + alias: apierrors + - pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 + alias: apiextensionsv1 + - pkg: k8s.io/apimachinery/pkg/util/runtime + alias: utilruntime + - pkg: ^k8s\.io/api/([^/]+)/(v[^/]+)$ + alias: $1$2 + - pkg: sigs.k8s.io/controller-runtime + alias: ctrl + - pkg: github.com/blang/semver/v4 + alias: bsemver + - pkg: github.com/operator-framework/library-olm/migration/pkg/migration + alias: migration + - pkg: github.com/operator-framework/library-olm/migration/pkg/catalogmigration + alias: catalogmigration + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + settings: + gci: + sections: + - standard + - dot + - default + - prefix(github.com/operator-framework/library-olm) + - prefix(github.com/operator-framework) + - localmodule + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a43931e --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +SHELL := /usr/bin/env bash -o pipefail +.SHELLFLAGS := -ec + +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +BIN_DIR := $(ROOT_DIR)/bin + +GOLANG_VERSION := $(shell sed -En 's/^go (.*)$$/\1/p' "go.mod") + +# bingo manages consistent tooling versions. +include .bingo/Variables.mk + +# Output paths for compiled CLI binaries +MIGRATE_OPERATORS_BIN := $(BIN_DIR)/migrate-operators-v0-to-v1 +MIGRATE_CATALOGS_BIN := $(BIN_DIR)/migrate-catalogs-v0-to-v1 + +##@ General + +.PHONY: help +help: ## Display this help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-28s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +.PHONY: build +build: build-migrate-operators build-migrate-catalogs ## Build both CLI binaries into bin/ + +.PHONY: build-migrate-operators +build-migrate-operators: ## Build migrate-operators-v0-to-v1 into bin/ + @mkdir -p $(BIN_DIR) + go build -o $(MIGRATE_OPERATORS_BIN) ./migration/examples/cmd/migrate-operators-v0-to-v1 + +.PHONY: build-migrate-catalogs +build-migrate-catalogs: ## Build migrate-catalogs-v0-to-v1 into bin/ + @mkdir -p $(BIN_DIR) + go build -o $(MIGRATE_CATALOGS_BIN) ./migration/examples/cmd/migrate-catalogs-v0-to-v1 + +.PHONY: build-all +build-all: ## Build and verify all packages (library + CLIs) + go build ./... + +##@ Test + +.PHONY: test +test: ## Run unit tests + go test ./... -count=1 + +.PHONY: test-verbose +test-verbose: ## Run unit tests with verbose output + go test ./... -v -count=1 + +##@ Lint & Verify + +.PHONY: lint +lint: $(GOLANGCI_LINT) ## Run golangci-lint + $(GOLANGCI_LINT) run ./... + +.PHONY: fmt +fmt: ## Run gofmt + go fmt ./... + +.PHONY: vet +vet: ## Run go vet + go vet ./... + +.PHONY: tidy +tidy: ## Run go mod tidy + go mod tidy + +.PHONY: verify +verify: tidy fmt vet lint ## Run all verification steps (tidy, fmt, vet, lint) + @git diff --exit-code || (echo "Files modified by verify — please commit the changes" && exit 1) + +.PHONY: api-diff +api-diff: $(GO_APIDIFF) ## Check for breaking API changes against origin/main + $(GO_APIDIFF) origin/main --repo-path=. --print-compatible + +##@ Clean + +.PHONY: clean +clean: ## Remove built binaries from bin/ + rm -f $(MIGRATE_OPERATORS_BIN) $(MIGRATE_CATALOGS_BIN) diff --git a/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go b/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go index b2053ba..035aecb 100644 --- a/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go +++ b/migration/examples/cmd/migrate-catalogs-v0-to-v1/main.go @@ -96,8 +96,8 @@ func runMigrateCatalogs(cmd *cobra.Command, _ []string) error { cm := catalogmigration.NewCatalogMigrator(c) opts := catalogmigration.CatalogMigratorOptions{ - DryRun: dryRun, - DeleteCatalogSource: deleteCatalogSource, + DryRun: dryRun, + DeleteCatalogSource: deleteCatalogSource, AcknowledgePriorityOverflow: acknowledgePriorityOverflow, } diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/check.go b/migration/examples/cmd/migrate-operators-v0-to-v1/check.go index d929427..aeb098d 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/check.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/check.go @@ -10,7 +10,6 @@ import ( ) var ( - checkSubscriptionName string checkSubscriptionNamespace string checkAll bool ) @@ -35,7 +34,7 @@ func init() { checkCmd.Flags().BoolVar(&checkAll, "all", false, "Check all Subscriptions on the cluster") } -func runCheck(cmd *cobra.Command, args []string) error { +func runCheck(cmd *cobra.Command, args []string) error { //nolint:nestif if checkAll && len(args) > 0 { return fmt.Errorf("cannot specify both an operator name and --all") } @@ -89,7 +88,7 @@ func runCheck(cmd *cobra.Command, args []string) error { sectionHeader("Compatibility Checks") _, csv, _, profileErr := m.GetCSVAndInstallPlan(ctx, opts) - if profileErr != nil { + if profileErr != nil { //nolint:nestif fail(fmt.Sprintf("Could not profile operator: %v", profileErr)) } else { propsJSON := csv.Annotations["operatorframework.io/properties"] diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go b/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go index 9f8724a..3f0bef6 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go @@ -5,8 +5,9 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/library-olm/migration/pkg/migration" ocv1 "github.com/operator-framework/operator-controller/api/v1" + + "github.com/operator-framework/library-olm/migration/pkg/migration" ) var cleanupAll bool @@ -33,7 +34,7 @@ func init() { cleanupCmd.Flags().BoolVar(&cleanupAll, "all", false, "Cleanup all Conflict-state ClusterExtensions") } -func runCleanup(cmd *cobra.Command, args []string) error { +func runCleanup(cmd *cobra.Command, args []string) error { //nolint:nestif if cleanupAll && len(args) > 0 { return fmt.Errorf("cannot specify both a CE name and --all") } @@ -50,7 +51,7 @@ func runCleanup(cmd *cobra.Command, args []string) error { m.Progress = progressFunc ctx := cmd.Context() - if cleanupAll { + if cleanupAll { //nolint:nestif // Find all CEs that are in Conflict state results, err := m.ScanAllSubscriptions(ctx) if err != nil { diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go index 7e987a1..d5a4b87 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go @@ -20,11 +20,11 @@ var ( convertInstallNs string // Acknowledgment flags - convertAckWatchScope bool - convertAckOpCond bool - convertAckOLMv0API bool - convertAckScopedSA bool - convertAckNotSteady bool + convertAckWatchScope bool + convertAckOpCond bool + convertAckOLMv0API bool + convertAckScopedSA bool + convertAckNotSteady bool ) var convertCmd = &cobra.Command{ @@ -62,7 +62,7 @@ func init() { convertCmd.Flags().BoolVar(&convertAckNotSteady, "acknowledge-not-steady-state", false, "Acknowledge that the operator is not at steady state") } -func runConvert(cmd *cobra.Command, args []string) error { +func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif if convertAll && len(args) > 0 { return fmt.Errorf("cannot specify both an operator name and --all") } @@ -79,7 +79,7 @@ func runConvert(cmd *cobra.Command, args []string) error { m.Progress = progressFunc ctx := cmd.Context() - if convertAll { + if convertAll { //nolint:nestif fmt.Printf("\n%s%s🔎 Scanning all Subscriptions for migration...%s\n", colorBold, colorCyan, colorReset) startProgress() results, err := m.ScanAllSubscriptions(ctx) @@ -139,7 +139,7 @@ func runConvert(cmd *cobra.Command, args []string) error { opts.ApplyDefaults() if convertDryRun { - return runConvertDryRun(cmd, m, opts, restCfg) + return runConvertDryRun(cmd, m, opts) } fmt.Printf("\n%s%s🔄 Migrating %s/%s to OLMv1...%s\n", colorBold, colorCyan, convertNamespace, operatorName, colorReset) @@ -257,7 +257,7 @@ func runConvert(cmd *cobra.Command, args []string) error { return nil } -func runConvertDryRun(cmd *cobra.Command, m *migration.Migrator, opts migration.Options, restCfg interface{}) error { +func runConvertDryRun(cmd *cobra.Command, m *migration.Migrator, opts migration.Options) error { ctx := cmd.Context() fmt.Printf("\n%s%s🔍 Dry run: %s/%s%s\n", colorBold, colorCyan, opts.SubscriptionNamespace, opts.SubscriptionName, colorReset) diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/output.go b/migration/examples/cmd/migrate-operators-v0-to-v1/output.go index 22ba1c5..4248e18 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/output.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/output.go @@ -80,10 +80,6 @@ func detail(key, value string) { fmt.Printf(" %s%-22s%s %s\n", colorDim, key, colorReset, value) } -func resource(kind, namespace, name string) { - fmt.Printf(" %s%s%s %s/%s\n", colorDim, kind, colorReset, namespace, name) -} - func printCheckResults(checks []migration.CheckResult) { for _, c := range checks { if c.Passed { diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go b/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go index fc64030..1d8312d 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go @@ -5,13 +5,13 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/library-olm/migration/pkg/migration" ocv1 "github.com/operator-framework/operator-controller/api/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/library-olm/migration/pkg/migration" ) var ( - rollbackAll bool + rollbackAll bool rollbackAcknowledgeInstalled bool ) @@ -37,7 +37,7 @@ func init() { rollbackCmd.Flags().BoolVar(&rollbackAcknowledgeInstalled, "acknowledge-installed", false, "Confirm rollback even when CE is Installed=True") } -func runRollback(cmd *cobra.Command, args []string) error { +func runRollback(cmd *cobra.Command, args []string) error { //nolint:nestif if rollbackAll && len(args) > 0 { return fmt.Errorf("cannot specify both a CE name and --all") } @@ -54,7 +54,7 @@ func runRollback(cmd *cobra.Command, args []string) error { m.Progress = progressFunc ctx := cmd.Context() - if rollbackAll { + if rollbackAll { //nolint:nestif var ceList ocv1.ClusterExtensionList if err := c.List(ctx, &ceList); err != nil { return fmt.Errorf("failed to list ClusterExtensions: %w", err) @@ -99,12 +99,3 @@ func runRollback(cmd *cobra.Command, args []string) error { fmt.Println() return nil } - -// rollbackSingleCE is a helper used when we have the CE object in hand. -func rollbackSingleCE(ctx interface{}, c client.Client, ce *ocv1.ClusterExtension, acknowledgeInstalled bool) error { - _ = c - _ = ce - _ = ctx - _ = acknowledgeInstalled - return nil -} diff --git a/migration/pkg/catalogmigration/catalogmigration.go b/migration/pkg/catalogmigration/catalogmigration.go index 88513a0..3e13628 100644 --- a/migration/pkg/catalogmigration/catalogmigration.go +++ b/migration/pkg/catalogmigration/catalogmigration.go @@ -26,8 +26,8 @@ const ( // CatalogMigratorOptions configures the catalog migration. type CatalogMigratorOptions struct { - DryRun bool - DeleteCatalogSource bool + DryRun bool + DeleteCatalogSource bool AcknowledgePriorityOverflow bool } @@ -52,9 +52,9 @@ func NewCatalogMigrator(c client.Client) *CatalogMigrator { // MigrateCatalogs processes all CatalogSources across all namespaces and maps them to ClusterCatalogs. // Strategy (per R8): -// - Same name + same image across namespaces → consolidate into a single ClusterCatalog -// - Same name + different image across namespaces → use - for each -// - Unique name → use metadata.name directly +// - Same name + same image across namespaces → consolidate into a single ClusterCatalog +// - Same name + different image across namespaces → use - for each +// - Unique name → use metadata.name directly func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigratorOptions) ([]CatalogMigrationResult, error) { // List all CatalogSources across all namespaces var csList operatorsv1alpha1.CatalogSourceList @@ -368,15 +368,15 @@ func convertPollInterval(cs operatorsv1alpha1.CatalogSource) int { } if cs.Spec.UpdateStrategy == nil || cs.Spec.UpdateStrategy.RegistryPoll == nil { - return 0 + return defaultPollMinutes } - interval := cs.Spec.UpdateStrategy.RegistryPoll.Interval + interval := cs.Spec.UpdateStrategy.Interval if interval == nil || interval.Duration == 0 { return 0 } - minutes := int(interval.Duration.Minutes()) + minutes := int(interval.Minutes()) if minutes < 1 { minutes = 1 } diff --git a/migration/pkg/migration/labels.go b/migration/pkg/migration/labels.go index 0d379bd..e301bc6 100644 --- a/migration/pkg/migration/labels.go +++ b/migration/pkg/migration/labels.go @@ -22,7 +22,7 @@ const ( LabelMetadataName = "olm.operatorframework.io/metadata.name" // SecretTypeObjectData is the Secret type for externalized COS object content. - SecretTypeObjectData = "olm.operatorframework.io/object-data" + SecretTypeObjectData = "olm.operatorframework.io/object-data" //nolint:gosec // G101 false positive: this is a Kubernetes Secret type identifier, not a credential // MigratedFromSubscriptionAnnotation is set on both the COS and CE. // Value is "/" of the source Subscription. diff --git a/migration/pkg/migration/scan.go b/migration/pkg/migration/scan.go index 8a40f26..77515c5 100644 --- a/migration/pkg/migration/scan.go +++ b/migration/pkg/migration/scan.go @@ -255,18 +255,6 @@ func EligibleFromScan(results []OperatorScanResult) []OperatorScanResult { return eligible } -// migrationAnnotatedCEsForSub returns ClusterExtension names that are annotated -// with the given subscription ref, or empty if none. -func migrationAnnotatedCEsForSub(ceList *ocv1.ClusterExtensionList, subRef string) []string { - var names []string - for _, ce := range ceList.Items { - if ref, ok := ce.Annotations[MigratedFromSubscriptionAnnotation]; ok && ref == subRef { - names = append(names, ce.Name) - } - } - return names -} - // RollbackClusterExtension deletes the CE and COS (orphan cascade), then restores the Subscription. func (m *Migrator) RollbackClusterExtension(ctx context.Context, ceName string, acknowledgeInstalled bool) error { var ce ocv1.ClusterExtension From 4c9653e05fd434084cce6cea67c903a95e45b8ae Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 21 Aug 2026 15:50:02 -0400 Subject: [PATCH 03/22] Fix R1 gaps: canonical API, acknowledge flags, backup annotations, C7 scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gap 1 — Acknowledge* flags and BackupDirectory in Options (types.go): Adds AcknowledgeWatchScopeChange, AcknowledgeOperatorCondition, AcknowledgeOLMv0APIAccess, AcknowledgeScopedServiceAccount, AcknowledgeNotSteadyState, AcknowledgeInstalled, and BackupDirectory. Backup struct gains OperatorGroup and InstallPlan fields and a SaveToDisk method that writes subscription.yaml, operatorgroup.yaml, clusterserviceversion.yaml, installplans/.yaml per R2.6. MigrationInfo gains SubscriptionBackupJSON and OperatorGroupBackupJSON to carry serialized specs to CreateClusterExtension. Gap 2 — Soft checks gate on acknowledge flags (compatibility.go, readiness.go): C1 (watch scope), C4 (OperatorCondition), C6 (scoped SA), C8 (steady state) now pass when the corresponding flag is set, with an "overridden (acknowledged)" message in the check result. Gap 3 — C7 catalog check in ScanAll + canonical R1.1 API (scan.go): ScanAllSubscriptions calls ResolveClusterCatalog after compatibility passes to classify operators without a serving ClusterCatalog as Ineligible (C7 hard block). Canonical methods added: ScanAll, Check, Gather, Rollback, Cleanup — the names specified in R1.1 — wrapping the existing implementations. Gap 4 — CE backup annotations + backup disk write (migration.go): BackupResources now fetches the OperatorGroup and accepts ip. Migrate serializes Subscription and OperatorGroup specs into MigrationInfo and calls SaveToDisk when BackupDirectory is set (warn-on-error, non- fatal per R2.6). CreateClusterExtension sets migration-subscription- backup, migration-operatorgroup-backup, and acknowledged-* annotations on the CE per R2.5, making rollback self-contained from CE state alone. Gap 5 — Wire ack flags and backup dir in convert.go: Both --all and single-operator flows assign all five AcknowledgeXxx fields and BackupDirectory from CLI flags to opts. Step 5 backup handling calls BackupResources with ip, populates bundleInfo backup JSON fields, and calls SaveToDisk when --backup is given. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- .../cmd/migrate-operators-v0-to-v1/convert.go | 49 +++++++++--- migration/pkg/migration/compatibility.go | 67 ++++++++++++----- migration/pkg/migration/labels.go | 10 +++ migration/pkg/migration/migration.go | 75 ++++++++++++++++--- migration/pkg/migration/readiness.go | 38 +++++++--- migration/pkg/migration/scan.go | 65 +++++++++++++++- migration/pkg/migration/types.go | 66 +++++++++++++++- 7 files changed, 318 insertions(+), 52 deletions(-) diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go index d5a4b87..4c6f068 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" @@ -104,8 +105,14 @@ func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif for _, r := range eligible { info(fmt.Sprintf("Migrating %s/%s...", r.SubscriptionNamespace, r.SubscriptionName)) opts := migration.Options{ - SubscriptionName: r.SubscriptionName, - SubscriptionNamespace: r.SubscriptionNamespace, + SubscriptionName: r.SubscriptionName, + SubscriptionNamespace: r.SubscriptionNamespace, + BackupDirectory: convertBackupDir, + AcknowledgeWatchScopeChange: convertAckWatchScope, + AcknowledgeOperatorCondition: convertAckOpCond, + AcknowledgeOLMv0APIAccess: convertAckOLMv0API, + AcknowledgeScopedServiceAccount: convertAckScopedSA, + AcknowledgeNotSteadyState: convertAckNotSteady, } opts.ApplyDefaults() @@ -131,10 +138,16 @@ func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif } opts := migration.Options{ - SubscriptionName: operatorName, - SubscriptionNamespace: convertNamespace, - ClusterExtensionName: convertCEName, - InstallNamespace: convertInstallNs, + SubscriptionName: operatorName, + SubscriptionNamespace: convertNamespace, + ClusterExtensionName: convertCEName, + InstallNamespace: convertInstallNs, + BackupDirectory: convertBackupDir, + AcknowledgeWatchScopeChange: convertAckWatchScope, + AcknowledgeOperatorCondition: convertAckOpCond, + AcknowledgeOLMv0APIAccess: convertAckOLMv0API, + AcknowledgeScopedServiceAccount: convertAckScopedSA, + AcknowledgeNotSteadyState: convertAckNotSteady, } opts.ApplyDefaults() @@ -206,12 +219,30 @@ func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif success(fmt.Sprintf("Found %d resources across %d kinds", len(objects), len(kindCounts))) stepHeader(5, "Backing up resources") - backup, err := m.BackupResources(ctx, opts, csv) + backup, err := m.BackupResources(ctx, opts, csv, ip) if err != nil { return fmt.Errorf("failed to backup resources: %w", err) } - _ = backup - success("Resources backed up in memory") + // Populate CE backup annotations (R2.5) — before PrepareForMigration deletes the Sub. + if backup.Subscription != nil { + if j, jErr := json.Marshal(backup.Subscription.Spec); jErr == nil { + bundleInfo.SubscriptionBackupJSON = string(j) + } + } + if backup.OperatorGroup != nil { + if j, jErr := json.Marshal(backup.OperatorGroup.Spec); jErr == nil { + bundleInfo.OperatorGroupBackupJSON = string(j) + } + } + // Disk backup (non-fatal per R2.6). + if convertBackupDir != "" { + if err := backup.SaveToDisk(convertBackupDir); err != nil { + warn(fmt.Sprintf("Backup to disk failed (CE annotation backup is authoritative): %v", err)) + } else { + success(fmt.Sprintf("Backup written to %s", convertBackupDir)) + } + } + success("Resources backed up in memory (CE annotation backup authoritative)") stepHeader(6, "Preparing operator for migration") info("Deleting Subscription and CSV (orphan cascade — workloads keep running)...") diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go index b666423..e827217 100644 --- a/migration/pkg/migration/compatibility.go +++ b/migration/pkg/migration/compatibility.go @@ -56,13 +56,21 @@ func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([] og := ogList.Items[0] var checks []CheckResult - // spec.serviceAccountName (C6) + // spec.serviceAccountName (C6 — soft) if og.Spec.ServiceAccountName != "" { - checks = append(checks, CheckResult{ - Name: "No scoped ServiceAccount", - Passed: false, - Message: "OperatorGroup has spec.serviceAccountName set; OLMv1 does not support scoped service accounts", - }) + if opts.AcknowledgeScopedServiceAccount { + checks = append(checks, CheckResult{ + Name: "No scoped ServiceAccount", + Passed: true, + Message: "overridden: operator will use cluster-admin (scoped ServiceAccount acknowledged)", + }) + } else { + checks = append(checks, CheckResult{ + Name: "No scoped ServiceAccount", + Passed: false, + Message: "OperatorGroup has spec.serviceAccountName set; OLMv1 does not support scoped service accounts", + }) + } } else { checks = append(checks, CheckResult{ Name: "No scoped ServiceAccount", @@ -101,13 +109,21 @@ func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([] }) } - // spec.targetNamespaces — AllNamespaces mode (C1) + // spec.targetNamespaces — AllNamespaces mode (C1 — soft) if len(og.Spec.TargetNamespaces) > 0 { - checks = append(checks, CheckResult{ - Name: "AllNamespaces mode", - Passed: false, - Message: "OperatorGroup has spec.targetNamespaces set; operator must be in AllNamespaces mode for migration", - }) + if opts.AcknowledgeWatchScopeChange { + checks = append(checks, CheckResult{ + Name: "AllNamespaces mode", + Passed: true, + Message: "overridden: operator will run AllNamespaces post-migration (watch scope change acknowledged)", + }) + } else { + checks = append(checks, CheckResult{ + Name: "AllNamespaces mode", + Passed: false, + Message: "OperatorGroup has spec.targetNamespaces set; operator must be in AllNamespaces mode for migration", + }) + } } else { checks = append(checks, CheckResult{ Name: "AllNamespaces mode", @@ -116,13 +132,21 @@ func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([] }) } - // status.namespaces warning — single-namespace targets will become AllNamespaces + // status.namespaces warning — single-namespace targets will become AllNamespaces (C1 — soft) if len(og.Status.Namespaces) == 1 && og.Status.Namespaces[0] != "" { - checks = append(checks, CheckResult{ - Name: "Namespace scope change", - Passed: false, - Message: fmt.Sprintf("OperatorGroup targets namespace %q; post-migration the operator will run in AllNamespaces mode", og.Status.Namespaces[0]), - }) + if opts.AcknowledgeWatchScopeChange { + checks = append(checks, CheckResult{ + Name: "Namespace scope change", + Passed: true, + Message: "overridden: watch scope change acknowledged", + }) + } else { + checks = append(checks, CheckResult{ + Name: "Namespace scope change", + Passed: false, + Message: fmt.Sprintf("OperatorGroup targets namespace %q; post-migration the operator will run in AllNamespaces mode", og.Status.Namespaces[0]), + }) + } } return checks, nil @@ -240,6 +264,13 @@ func (m *Migrator) checkNoOperatorConditions(ctx context.Context, opts Options, } if len(oc.Status.Conditions) > 0 { + if opts.AcknowledgeOperatorCondition { + return CheckResult{ + Name: "No OperatorCondition usage", + Passed: true, + Message: "overridden: active OperatorCondition usage acknowledged", + }, nil + } return CheckResult{ Name: "No OperatorCondition usage", Passed: false, diff --git a/migration/pkg/migration/labels.go b/migration/pkg/migration/labels.go index e301bc6..c95aaf3 100644 --- a/migration/pkg/migration/labels.go +++ b/migration/pkg/migration/labels.go @@ -28,6 +28,16 @@ const ( // Value is "/" of the source Subscription. MigratedFromSubscriptionAnnotation = "olm.operatorframework.io/migrated-from-subscription" + // MigrationSubscriptionBackupAnnotation holds JSON-encoded Subscription spec on the CE (R2.5). + MigrationSubscriptionBackupAnnotation = "olm.operatorframework.io/migration-subscription-backup" + + // MigrationOperatorGroupBackupAnnotation holds JSON-encoded OperatorGroup spec on the CE (R2.5). + MigrationOperatorGroupBackupAnnotation = "olm.operatorframework.io/migration-operatorgroup-backup" + + // AnnotationAcknowledgedPrefix is the prefix for per-flag audit annotations on the CE (R2.5). + // Full key: AnnotationAcknowledgedPrefix + "", value "true". + AnnotationAcknowledgedPrefix = "olm.operatorframework.io/acknowledged-" + // MigratedFromCatalogSourceAnnotation is set on ClusterCatalog by the catalog migration tool. MigratedFromCatalogSourceAnnotation = "olm.operatorframework.io/migrated-from-catalogsource" diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index fc7d525..81a17f1 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -73,11 +73,30 @@ func (m *Migrator) Migrate(ctx context.Context, opts Options) error { } info.ResolvedCatalogName = catalogName - backup, err := m.BackupResources(ctx, opts, csv) + backup, err := m.BackupResources(ctx, opts, csv, ip) if err != nil { return fmt.Errorf("failed to backup resources: %w", err) } + // Populate CE backup annotations (R2.5) — must happen before PrepareForMigration deletes the Sub. + if backup.Subscription != nil { + if j, err := json.Marshal(backup.Subscription.Spec); err == nil { + info.SubscriptionBackupJSON = string(j) + } + } + if backup.OperatorGroup != nil { + if j, err := json.Marshal(backup.OperatorGroup.Spec); err == nil { + info.OperatorGroupBackupJSON = string(j) + } + } + + // Disk backup (non-fatal per R2.6 — CE annotation backup is authoritative). + if opts.BackupDirectory != "" { + if err := backup.SaveToDisk(opts.BackupDirectory); err != nil { + m.progress(fmt.Sprintf("Warning: backup to disk failed (CE annotation backup is authoritative): %v", err)) + } + } + if err := m.PrepareForMigration(ctx, opts, csv); err != nil { if recoverErr := m.RecoverFromBackup(ctx, opts, backup); recoverErr != nil { return fmt.Errorf("preparation failed: %w; recovery also failed: %v", err, recoverErr) @@ -128,8 +147,9 @@ func (m *Migrator) EnsurePrerequisites(ctx context.Context, opts Options) (*oper return csv, ip, readiness, compat, nil } -// BackupResources creates in-memory backup copies of the Subscription and CSV for recovery. -func (m *Migrator) BackupResources(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) (*Backup, error) { +// BackupResources creates in-memory backup copies of the Subscription, CSV, OperatorGroup, +// and InstallPlan for recovery and auditing (R1.8, R2.6). +func (m *Migrator) BackupResources(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion, ip *operatorsv1alpha1.InstallPlan) (*Backup, error) { var sub operatorsv1alpha1.Subscription if err := m.Client.Get(ctx, types.NamespacedName{ Name: opts.SubscriptionName, @@ -138,9 +158,19 @@ func (m *Migrator) BackupResources(ctx context.Context, opts Options, csv *opera return nil, fmt.Errorf("failed to backup Subscription: %w", err) } + // Best-effort: fetch the OperatorGroup from the Subscription namespace. + var ogList operatorsv1.OperatorGroupList + _ = m.Client.List(ctx, &ogList, client.InNamespace(opts.SubscriptionNamespace)) + var og *operatorsv1.OperatorGroup + if len(ogList.Items) > 0 { + og = ogList.Items[0].DeepCopy() + } + return &Backup{ Subscription: sub.DeepCopy(), ClusterServiceVersion: csv.DeepCopy(), + OperatorGroup: og, + InstallPlan: ip, }, nil } @@ -299,16 +329,41 @@ func (m *Migrator) WaitForCOSSucceeded(ctx context.Context, cosName string) erro }) } -// CreateClusterExtension creates a CE that adopts the COS. -// ServiceAccount is NOT set (deprecated and ignored in OLMv1). -// Migration annotations are added to the CE for AlreadyMigrated/Conflict detection. +// CreateClusterExtension creates a CE that adopts the COS (R2.3). +// spec.serviceAccount is NOT set — deprecated and ignored in OLMv1 (R2.5/R7). +// Migration annotations (R2.5) are added for AlreadyMigrated/Conflict detection and rollback. func (m *Migrator) CreateClusterExtension(ctx context.Context, opts Options, info *MigrationInfo) error { + // Build annotations (R2.5). + annotations := map[string]string{ + MigratedFromSubscriptionAnnotation: fmt.Sprintf("%s/%s", opts.SubscriptionNamespace, opts.SubscriptionName), + } + if info.SubscriptionBackupJSON != "" { + annotations[MigrationSubscriptionBackupAnnotation] = info.SubscriptionBackupJSON + } + if info.OperatorGroupBackupJSON != "" { + annotations[MigrationOperatorGroupBackupAnnotation] = info.OperatorGroupBackupJSON + } + // Record which eligibility-override flags were acknowledged (audit trail). + if opts.AcknowledgeWatchScopeChange { + annotations[AnnotationAcknowledgedPrefix+"watch-scope-change"] = "true" + } + if opts.AcknowledgeOperatorCondition { + annotations[AnnotationAcknowledgedPrefix+"operator-condition"] = "true" + } + if opts.AcknowledgeOLMv0APIAccess { + annotations[AnnotationAcknowledgedPrefix+"olmv0-api-access"] = "true" + } + if opts.AcknowledgeScopedServiceAccount { + annotations[AnnotationAcknowledgedPrefix+"scoped-serviceaccount"] = "true" + } + if opts.AcknowledgeNotSteadyState { + annotations[AnnotationAcknowledgedPrefix+"not-steady-state"] = "true" + } + ce := &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ - Name: opts.ClusterExtensionName, - Annotations: map[string]string{ - MigratedFromSubscriptionAnnotation: fmt.Sprintf("%s/%s", opts.SubscriptionNamespace, opts.SubscriptionName), - }, + Name: opts.ClusterExtensionName, + Annotations: annotations, }, Spec: ocv1.ClusterExtensionSpec{ Namespace: opts.InstallNamespace, diff --git a/migration/pkg/migration/readiness.go b/migration/pkg/migration/readiness.go index 166781e..e8cef55 100644 --- a/migration/pkg/migration/readiness.go +++ b/migration/pkg/migration/readiness.go @@ -97,7 +97,7 @@ func (m *Migrator) CheckReadiness(ctx context.Context, opts Options) (*PreMigrat } // CSV phase and reason - if sub.Status.InstalledCSV != "" { + if sub.Status.InstalledCSV != "" { //nolint:nestif csvName := sub.Status.InstalledCSV var csv operatorsv1alpha1.ClusterServiceVersion if err := m.Client.Get(ctx, types.NamespacedName{ @@ -110,17 +110,33 @@ func (m *Migrator) CheckReadiness(ctx context.Context, opts Options) (*PreMigrat Message: fmt.Sprintf("failed to get CSV %s: %v", csvName, err), }) } else if csv.Status.Phase != operatorsv1alpha1.CSVPhaseSucceeded { - report.Checks = append(report.Checks, CheckResult{ - Name: "CSV health", - Passed: false, - Message: fmt.Sprintf("phase is %q, expected %q", csv.Status.Phase, operatorsv1alpha1.CSVPhaseSucceeded), - }) + if opts.AcknowledgeNotSteadyState { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: true, + Message: fmt.Sprintf("overridden: phase is %q (not at steady state acknowledged)", csv.Status.Phase), + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: false, + Message: fmt.Sprintf("phase is %q, expected %q", csv.Status.Phase, operatorsv1alpha1.CSVPhaseSucceeded), + }) + } } else if csv.Status.Reason != operatorsv1alpha1.CSVReasonInstallSuccessful { - report.Checks = append(report.Checks, CheckResult{ - Name: "CSV health", - Passed: false, - Message: fmt.Sprintf("reason is %q, expected %q", csv.Status.Reason, operatorsv1alpha1.CSVReasonInstallSuccessful), - }) + if opts.AcknowledgeNotSteadyState { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: true, + Message: fmt.Sprintf("overridden: reason is %q (not at steady state acknowledged)", csv.Status.Reason), + }) + } else { + report.Checks = append(report.Checks, CheckResult{ + Name: "CSV health", + Passed: false, + Message: fmt.Sprintf("reason is %q, expected %q", csv.Status.Reason, operatorsv1alpha1.CSVReasonInstallSuccessful), + }) + } } else { report.Checks = append(report.Checks, CheckResult{ Name: "CSV health", diff --git a/migration/pkg/migration/scan.go b/migration/pkg/migration/scan.go index 77515c5..a152203 100644 --- a/migration/pkg/migration/scan.go +++ b/migration/pkg/migration/scan.go @@ -115,11 +115,34 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu continue } - // Merge failed checks + // Merge readiness + compat failed checks result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) + + // C7: catalog availability (hard check — no override) + // Only run if readiness+compat pass; avoids noisy catalog errors for clearly ineligible operators. if len(result.FailedChecks) == 0 { - result.Status = OperatorStatusEligible - result.Eligible = true + catalogName, catalogErr := m.ResolveClusterCatalog(ctx, &MigrationInfo{ + PackageName: sub.Spec.Package, + Channel: sub.Spec.Channel, + Version: result.Version, + }, m.RESTConfig) + if catalogErr != nil { + result.FailedChecks = append(result.FailedChecks, CheckResult{ + Name: "Catalog availability", + Passed: false, + Message: fmt.Sprintf("package not found in any serving ClusterCatalog; run migrate-catalogs-v0-to-v1 first: %v", catalogErr), + }) + result.Status = OperatorStatusIneligible + result.Eligible = false + } else { + result.FailedChecks = append(result.FailedChecks, CheckResult{ + Name: "Catalog availability", + Passed: true, + Message: fmt.Sprintf("package available in ClusterCatalog %s", catalogName), + }) + result.Status = OperatorStatusEligible + result.Eligible = true + } } else { result.Status = OperatorStatusIneligible result.Eligible = false @@ -382,3 +405,39 @@ func splitNamespacedName(ref string) (string, string, error) { func unmarshalJSON(data string, v interface{}) error { return json.Unmarshal([]byte(data), v) } + +// ── Canonical R1.1 library API ──────────────────────────────────────────────── + +// ScanAll classifies all OLMv0 Subscriptions into the four states (R1.1), +// including catalog-availability (C7) per operator. +func (m *Migrator) ScanAll(ctx context.Context) ([]OperatorScanResult, error) { + return m.ScanAllSubscriptions(ctx) +} + +// Check runs all readiness, compatibility, and catalog-availability checks for +// one operator without mutating the cluster (R1.1). +func (m *Migrator) Check(ctx context.Context, opts Options) (*OperatorScanResult, error) { + opts.ApplyDefaults() + return m.ScanSubscription(ctx, opts) +} + +// Gather collects and returns everything that would be migrated without making +// any cluster mutations — backs the CLI convert --dry-run (R1.1). +func (m *Migrator) Gather(ctx context.Context, opts Options) (*MigrationInfo, error) { + opts.ApplyDefaults() + return m.GatherMigrationInfo(ctx, opts) +} + +// Rollback restores an operator to OLMv0 management (R1.1). +// opts.AcknowledgeInstalled must be true when the CE is Installed=True. +func (m *Migrator) Rollback(ctx context.Context, opts Options) error { + opts.ApplyDefaults() + return m.RollbackClusterExtension(ctx, opts.ClusterExtensionName, opts.AcknowledgeInstalled) +} + +// Cleanup finishes a partial migration in Conflict state by deleting the +// Subscription and OLMv0 artifacts, leaving the CE intact (R1.1). +func (m *Migrator) Cleanup(ctx context.Context, opts Options) error { + opts.ApplyDefaults() + return m.CleanupConflict(ctx, opts.ClusterExtensionName) +} diff --git a/migration/pkg/migration/types.go b/migration/pkg/migration/types.go index 292c502..1e095e4 100644 --- a/migration/pkg/migration/types.go +++ b/migration/pkg/migration/types.go @@ -1,11 +1,17 @@ package migration import ( + "fmt" + "os" + "path/filepath" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" ) @@ -25,6 +31,20 @@ type Options struct { SubscriptionNamespace string ClusterExtensionName string InstallNamespace string + + // BackupDirectory, when non-empty, writes OLM objects to disk before deletions (R2.6). + BackupDirectory string + + // Soft eligibility override flags (R3). Setting a flag records an + // acknowledged-:"true" annotation on the CE for audit (R2.5). + AcknowledgeWatchScopeChange bool // C1 + AcknowledgeOperatorCondition bool // C4 + AcknowledgeOLMv0APIAccess bool // C5 + AcknowledgeScopedServiceAccount bool // C6 + AcknowledgeNotSteadyState bool // C8 + + // AcknowledgeInstalled is required for Rollback when the CE is Installed=True. + AcknowledgeInstalled bool } // ApplyDefaults fills in default values for any unset optional fields. @@ -49,6 +69,11 @@ type MigrationInfo struct { CatalogSourceImage string // tag-based image from CatalogSource.Spec.Image ResolvedCatalogName string CollectedObjects []unstructured.Unstructured + + // Subscription spec JSON for the CE migration-subscription-backup annotation (R2.5). + SubscriptionBackupJSON string + // OperatorGroup spec JSON for the CE migration-operatorgroup-backup annotation (R2.5). + OperatorGroupBackupJSON string } // ProgressFunc is called periodically during wait operations to report status. @@ -72,8 +97,47 @@ func (m *Migrator) progress(msg string) { } } -// Backup holds serialized copies of resources for recovery. +// Backup holds serialized copies of OLMv0 resources for recovery and auditing. type Backup struct { Subscription *operatorsv1alpha1.Subscription ClusterServiceVersion *operatorsv1alpha1.ClusterServiceVersion + OperatorGroup *operatorsv1.OperatorGroup + InstallPlan *operatorsv1alpha1.InstallPlan +} + +// SaveToDisk writes backup files to dir, creating it if absent. Per R2.6, +// failures here are non-fatal — the CE annotation backup is the authoritative path. +func (b *Backup) SaveToDisk(dir string) error { + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("failed to create backup directory: %w", err) + } + if err := writeYAMLFile(filepath.Join(dir, "subscription.yaml"), b.Subscription); err != nil { + return fmt.Errorf("failed to write subscription.yaml: %w", err) + } + if b.OperatorGroup != nil { + if err := writeYAMLFile(filepath.Join(dir, "operatorgroup.yaml"), b.OperatorGroup); err != nil { + return fmt.Errorf("failed to write operatorgroup.yaml: %w", err) + } + } + if err := writeYAMLFile(filepath.Join(dir, "clusterserviceversion.yaml"), b.ClusterServiceVersion); err != nil { + return fmt.Errorf("failed to write clusterserviceversion.yaml: %w", err) + } + if b.InstallPlan != nil { + ipDir := filepath.Join(dir, "installplans") + if err := os.MkdirAll(ipDir, 0o750); err != nil { + return fmt.Errorf("failed to create installplans directory: %w", err) + } + if err := writeYAMLFile(filepath.Join(ipDir, b.InstallPlan.Name+".yaml"), b.InstallPlan); err != nil { + return fmt.Errorf("failed to write installplan: %w", err) + } + } + return nil +} + +func writeYAMLFile(path string, obj interface{}) error { + data, err := yaml.Marshal(obj) + if err != nil { + return err + } + return os.WriteFile(path, data, 0o600) } From ca0c812eaf85b799d08bbedc53f3d013b9339772 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 21 Aug 2026 16:05:12 -0400 Subject: [PATCH 04/22] Fix remaining R1 gaps: C7 in Check, DeleteOperatorGroup, Reason field R1.1 Check() missing C7: ScanSubscription now calls ResolveClusterCatalog after all readiness and compatibility checks pass. A missing ClusterCatalog is reported as Ineligible with a clear "package not found" reason, matching the hard C7 block in REQUIREMENTS.md R3. The sub variable is now captured from GetCSVAndInstallPlan so the correct channel is passed to the catalog query. R1.2 --delete-operatorgroup no-op: DeleteOperatorGroup bool added to Options. cleanupOperatorGroup now returns Skipped immediately when the flag is false, implementing the R6 requirement that both conditions must be met (flag set AND no other Subscriptions remain). Both single-operator and --all convert flows now assign convertDeleteOG to opts.DeleteOperatorGroup. R1.3 human-readable Reason field: OperatorScanResult gains a Reason string field populated for every classification: Conflict, Ineligible (with specific cause), Eligible, and AlreadyMigrated. This is alongside Error for backwards compatibility; library consumers can use Reason directly without calling .Error() on a nil check. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- .../cmd/migrate-operators-v0-to-v1/convert.go | 2 + migration/pkg/migration/migration.go | 12 +++- migration/pkg/migration/scan.go | 67 ++++++++++++++----- migration/pkg/migration/types.go | 4 ++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go index 4c6f068..6e78cb2 100644 --- a/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go @@ -108,6 +108,7 @@ func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif SubscriptionName: r.SubscriptionName, SubscriptionNamespace: r.SubscriptionNamespace, BackupDirectory: convertBackupDir, + DeleteOperatorGroup: convertDeleteOG, AcknowledgeWatchScopeChange: convertAckWatchScope, AcknowledgeOperatorCondition: convertAckOpCond, AcknowledgeOLMv0APIAccess: convertAckOLMv0API, @@ -143,6 +144,7 @@ func runConvert(cmd *cobra.Command, args []string) error { //nolint:nestif ClusterExtensionName: convertCEName, InstallNamespace: convertInstallNs, BackupDirectory: convertBackupDir, + DeleteOperatorGroup: convertDeleteOG, AcknowledgeWatchScopeChange: convertAckWatchScope, AcknowledgeOperatorCondition: convertAckOpCond, AcknowledgeOLMv0APIAccess: convertAckOLMv0API, diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index 81a17f1..fd086d5 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -522,10 +522,20 @@ func (m *Migrator) deleteOperatorCondition(ctx context.Context, csvName, namespa return nil } -// cleanupOperatorGroup deletes the OperatorGroup if no other Subscriptions remain in the namespace. +// cleanupOperatorGroup deletes the OperatorGroup when both --delete-operatorgroup is set +// AND no other Subscriptions remain in the namespace (R6). func (m *Migrator) cleanupOperatorGroup(ctx context.Context, opts Options) []CleanupAction { var actions []CleanupAction + // Both conditions required per R6: flag must be set AND no remaining Subscriptions. + if !opts.DeleteOperatorGroup { + actions = append(actions, CleanupAction{ + Description: "Delete OperatorGroup (skipped: --delete-operatorgroup not set)", + Skipped: true, + }) + return actions + } + var subList operatorsv1alpha1.SubscriptionList if err := m.Client.List(ctx, &subList, client.InNamespace(opts.SubscriptionNamespace)); err != nil { actions = append(actions, CleanupAction{ diff --git a/migration/pkg/migration/scan.go b/migration/pkg/migration/scan.go index a152203..e4ab3fb 100644 --- a/migration/pkg/migration/scan.go +++ b/migration/pkg/migration/scan.go @@ -20,6 +20,7 @@ type OperatorScanResult struct { Version string State string Status OperatorStatus // four-state classification + Reason string // human-readable explanation of the status (R1.3) Eligible bool // true when Status == Eligible (backwards compat) Error error FailedChecks []CheckResult @@ -71,8 +72,9 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu // Conflict: both Subscription and annotated CE exist if _, hasCE := migratedCEBySubRef[subRef]; hasCE { result.Status = OperatorStatusConflict + result.Reason = "both Subscription and annotated ClusterExtension exist; resolve with cleanup or rollback" result.Eligible = false - result.Error = fmt.Errorf("both Subscription and annotated ClusterExtension exist; resolve with cleanup or rollback") + result.Error = fmt.Errorf("%s", result.Reason) results = append(results, result) continue } @@ -89,6 +91,7 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu readiness, err := m.CheckReadiness(ctx, opts) if err != nil { result.Status = OperatorStatusIneligible + result.Reason = err.Error() result.Error = err results = append(results, result) continue @@ -98,6 +101,7 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu _, csv, _, err := m.GetCSVAndInstallPlan(ctx, opts) if err != nil { result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("failed to get CSV: %v", err) result.Error = fmt.Errorf("failed to get CSV: %w", err) results = append(results, result) continue @@ -110,6 +114,7 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) if err != nil { result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("compatibility check error: %v", err) result.Error = fmt.Errorf("compatibility check error: %w", err) results = append(results, result) continue @@ -118,8 +123,8 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu // Merge readiness + compat failed checks result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) - // C7: catalog availability (hard check — no override) - // Only run if readiness+compat pass; avoids noisy catalog errors for clearly ineligible operators. + // C7: catalog availability (hard check — no override). + // Only run when readiness+compat pass to avoid noisy catalog errors for clearly ineligible operators. if len(result.FailedChecks) == 0 { catalogName, catalogErr := m.ResolveClusterCatalog(ctx, &MigrationInfo{ PackageName: sub.Spec.Package, @@ -133,6 +138,7 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu Message: fmt.Sprintf("package not found in any serving ClusterCatalog; run migrate-catalogs-v0-to-v1 first: %v", catalogErr), }) result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("package %q not found in any serving ClusterCatalog", sub.Spec.Package) result.Eligible = false } else { result.FailedChecks = append(result.FailedChecks, CheckResult{ @@ -141,10 +147,12 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu Message: fmt.Sprintf("package available in ClusterCatalog %s", catalogName), }) result.Status = OperatorStatusEligible + result.Reason = "passes all readiness, compatibility, and catalog-availability checks" result.Eligible = true } } else { result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("%d check(s) failed", len(result.FailedChecks)) result.Eligible = false } results = append(results, result) @@ -156,12 +164,11 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu continue // handled above as Conflict or normal sub } results = append(results, OperatorScanResult{ - SubscriptionName: ceName, - SubscriptionNamespace: "", - PackageName: "", - Status: OperatorStatusAlreadyMigrated, - Eligible: false, - State: fmt.Sprintf("ClusterExtension %s (migrated from %s)", ceName, subRef), + SubscriptionName: ceName, + Status: OperatorStatusAlreadyMigrated, + Reason: fmt.Sprintf("ClusterExtension %s exists with migrated-from-subscription annotation; Subscription is gone", ceName), + Eligible: false, + State: fmt.Sprintf("ClusterExtension %s (migrated from %s)", ceName, subRef), }) } @@ -186,7 +193,8 @@ func (m *Migrator) ScanSubscription(ctx context.Context, opts Options) (*Operato for _, ce := range ceList.Items { if ref, ok := ce.Annotations[MigratedFromSubscriptionAnnotation]; ok && ref == subRef { result.Status = OperatorStatusConflict - result.Error = fmt.Errorf("both Subscription and annotated ClusterExtension %s exist; resolve with cleanup or rollback", ce.Name) + result.Reason = fmt.Sprintf("both Subscription and annotated ClusterExtension %s exist; resolve with cleanup or rollback", ce.Name) + result.Error = fmt.Errorf("%s", result.Reason) return result, nil } } @@ -196,14 +204,15 @@ func (m *Migrator) ScanSubscription(ctx context.Context, opts Options) (*Operato return nil, err } - _, csv, _, err := m.GetCSVAndInstallPlan(ctx, opts) + sub, csv, _, err := m.GetCSVAndInstallPlan(ctx, opts) if err != nil { result.Status = OperatorStatusIneligible + result.Reason = err.Error() result.Error = err return result, nil } - result.PackageName = csv.Spec.Description + result.PackageName = sub.Spec.Package result.InstalledCSV = csv.Name result.Version = parseCSVVersion(csv) @@ -211,17 +220,43 @@ func (m *Migrator) ScanSubscription(ctx context.Context, opts Options) (*Operato compat, err := m.CheckCompatibility(ctx, opts, csv, propsJSON) if err != nil { result.Status = OperatorStatusIneligible + result.Reason = err.Error() result.Error = err return result, nil } result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) - if len(result.FailedChecks) == 0 { - result.Status = OperatorStatusEligible - result.Eligible = true - } else { + if len(result.FailedChecks) > 0 { result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("%d check(s) failed", len(result.FailedChecks)) + return result, nil } + + // C7: catalog availability (hard check — no override) + catalogName, catalogErr := m.ResolveClusterCatalog(ctx, &MigrationInfo{ + PackageName: result.PackageName, + Channel: sub.Spec.Channel, + Version: result.Version, + }, m.RESTConfig) + if catalogErr != nil { + result.FailedChecks = append(result.FailedChecks, CheckResult{ + Name: "Catalog availability", + Passed: false, + Message: fmt.Sprintf("package not found in any serving ClusterCatalog; run migrate-catalogs-v0-to-v1 first: %v", catalogErr), + }) + result.Status = OperatorStatusIneligible + result.Reason = fmt.Sprintf("package %q not found in any serving ClusterCatalog", result.PackageName) + return result, nil + } + + result.FailedChecks = append(result.FailedChecks, CheckResult{ + Name: "Catalog availability", + Passed: true, + Message: fmt.Sprintf("package available in ClusterCatalog %s", catalogName), + }) + result.Status = OperatorStatusEligible + result.Reason = "passes all readiness, compatibility, and catalog-availability checks" + result.Eligible = true return result, nil } diff --git a/migration/pkg/migration/types.go b/migration/pkg/migration/types.go index 1e095e4..a4fbe09 100644 --- a/migration/pkg/migration/types.go +++ b/migration/pkg/migration/types.go @@ -45,6 +45,10 @@ type Options struct { // AcknowledgeInstalled is required for Rollback when the CE is Installed=True. AcknowledgeInstalled bool + + // DeleteOperatorGroup deletes the OperatorGroup when both this flag is set AND + // no other Subscriptions remain in the namespace (R6). + DeleteOperatorGroup bool } // ApplyDefaults fills in default values for any unset optional fields. From 2ff1e7527ad75fdb8ca97e7b4a5feedfab09701b Mon Sep 17 00:00:00 2001 From: Todd Short Date: Fri, 21 Aug 2026 16:40:44 -0400 Subject: [PATCH 05/22] Remove C3 hard block: OLMv1 now supports APIService definitions (OPRUN-4723) The registry+v1 renderer in operator-controller now generates APIService objects natively via BundleCSVAPIServiceGenerator (OPRUN-4723). Operators that declare spec.apiservicedefinitions.owned are no longer blocked and become Eligible with no override flag required. Removes checkNoAPIServices from CheckCompatibility and deletes the function. Replaces the C3 call site with an explanatory comment referencing OPRUN-4723. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/compatibility.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go index e827217..1d6dbb4 100644 --- a/migration/pkg/migration/compatibility.go +++ b/migration/pkg/migration/compatibility.go @@ -27,8 +27,9 @@ func (m *Migrator) CheckCompatibility(ctx context.Context, opts Options, csv *op // Dependency checks (C2 — hard block) report.Checks = append(report.Checks, checkNoDependencies(bundleProperties)...) - // APIService checks (C3 — hard block, temporary until OPRUN-4723) - report.Checks = append(report.Checks, checkNoAPIServices(csv)) + // C3 (APIService definitions) was removed: OLMv1 now manages APIService objects + // natively via the registry+v1 renderer (OPRUN-4723). Operators with owned + // APIService definitions are now Eligible with no override required. // OperatorCondition checks (C4) condCheck, err := m.checkNoOperatorConditions(ctx, opts, csv) @@ -228,22 +229,6 @@ func checkNoDependencies(propertiesJSON string) []CheckResult { return issues } -// checkNoAPIServices enforces C3 — no APIService definitions (hard block, temporary until OPRUN-4723). -func checkNoAPIServices(csv *operatorsv1alpha1.ClusterServiceVersion) CheckResult { - if len(csv.Spec.APIServiceDefinitions.Owned) > 0 || len(csv.Spec.APIServiceDefinitions.Required) > 0 { - return CheckResult{ - Name: "No APIService definitions", - Passed: false, - Message: "CSV has spec.apiservicedefinitions set; OLMv1 does not yet support APIService definitions (tracked by OPRUN-4723)", - } - } - return CheckResult{ - Name: "No APIService definitions", - Passed: true, - Message: "CSV does not define APIServices", - } -} - // checkNoOperatorConditions enforces C4 — no active OperatorCondition status entries. // RBAC presence alone is NOT treated as usage; only status.conditions entries count. func (m *Migrator) checkNoOperatorConditions(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) (CheckResult, error) { From d750650b05956d2a98a2b092ba66a2a01a699614 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 13:34:54 -0400 Subject: [PATCH 06/22] Add OWNERS, GitHub workflows, and repo scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OWNERS: flattened from operator-controller (approvers and reviewers inline, no aliases file). .github/: dependabot.yml — daily updates for GitHub Actions and gomod, k8s deps grouped, 14-day cooldown pull_request_template — standard checklist; no emoji-prefix convention ISSUE_TEMPLATE/ — Kubernetes slack contact link workflows/sanity.yaml — verify + lint on PR/push/merge-group workflows/unit-test.yaml — go test ./... on PR/push/merge-group workflows/go-apidiff.yaml — breaking API check, overridable via label workflows/go-verdiff.yaml — guards against unreviewed Go version bumps, overridable via go-verdiff-override label workflows/stale.yml — mark stale at 90d, close at 120d hack/tools/check-go-version.sh: copied from operator-controller; used by go-verdiff workflow to detect root go.mod Go version changes. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- .github/ISSUE_TEMPLATE/config.yml | 6 ++ .github/dependabot.yml | 23 ++++++ .github/pull_request_template.md | 14 ++++ .github/workflows/go-apidiff.yaml | 35 +++++++++ .github/workflows/go-verdiff.yaml | 36 +++++++++ .github/workflows/sanity.yaml | 30 ++++++++ .github/workflows/stale.yml | 31 ++++++++ .github/workflows/unit-test.yaml | 20 +++++ OWNERS | 18 +++++ hack/tools/check-go-version.sh | 120 ++++++++++++++++++++++++++++++ 10 files changed, 333 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/go-apidiff.yaml create mode 100644 .github/workflows/go-verdiff.yaml create mode 100644 .github/workflows/sanity.yaml create mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/unit-test.yaml create mode 100644 OWNERS create mode 100755 hack/tools/check-go-version.sh diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cbfb216 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +contact_links: + - name: Kubernetes slack + url: https://slack.k8s.io/ + about: | + Join us on #olm-dev for discussions related to OLM Development or on + #kubernetes-operators for discussions related to the Operator pattern of Kubernetes API extension. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..79f719e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 14 + commit-message: + prefix: "chore" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 14 + commit-message: + prefix: "chore" + groups: + k8s-dependencies: + patterns: + - "k8s.io/*" + - "sigs.k8s.io/*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1d759c9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +# Description + + + +## Reviewer Checklist + +- [ ] API Go Documentation +- [ ] Tests: Unit Tests (and E2E Tests, if appropriate) +- [ ] Comprehensive Commit Messages +- [ ] Links to related GitHub Issue(s) diff --git a/.github/workflows/go-apidiff.yaml b/.github/workflows/go-apidiff.yaml new file mode 100644 index 0000000..23f97eb --- /dev/null +++ b/.github/workflows/go-apidiff.yaml @@ -0,0 +1,35 @@ +name: go-apidiff + +on: + merge_group: + pull_request: + +jobs: + go-apidiff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: joelanford/go-apidiff@main + id: check-api + continue-on-error: true + - name: Check for override label + if: ${{ steps.check-api.outcome == 'failure' }} + run: | + if gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name' | grep -q "${OVERRIDE_LABEL}"; then + echo "Found ${OVERRIDE_LABEL} label, overriding failed results." + exit 0 + else + echo "No ${OVERRIDE_LABEL} label found, failing the job." + exit 1 + fi + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + PR: ${{ github.event.pull_request.number }} + OVERRIDE_LABEL: "go-apidiff-override" diff --git a/.github/workflows/go-verdiff.yaml b/.github/workflows/go-verdiff.yaml new file mode 100644 index 0000000..893fde5 --- /dev/null +++ b/.github/workflows/go-verdiff.yaml @@ -0,0 +1,36 @@ +name: go-verdiff + +on: + pull_request: + branches: + - main + +jobs: + go-verdiff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check golang version + id: check-version + continue-on-error: true + run: | + hack/tools/check-go-version.sh -b "${{ github.event.pull_request.base.sha }}" + shell: bash + - name: Check for override label + if: ${{ steps.check-version.outcome == 'failure' }} + run: | + if gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name' | grep -q "${OVERRIDE_LABEL}"; then + echo "Found ${OVERRIDE_LABEL} label, overriding failed results." + exit 0 + else + echo "No ${OVERRIDE_LABEL} label found, failing the job." + exit 1 + fi + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + PR: ${{ github.event.pull_request.number }} + OVERRIDE_LABEL: "go-verdiff-override" diff --git a/.github/workflows/sanity.yaml b/.github/workflows/sanity.yaml new file mode 100644 index 0000000..348bd76 --- /dev/null +++ b/.github/workflows/sanity.yaml @@ -0,0 +1,30 @@ +name: sanity + +on: + workflow_dispatch: + pull_request: + merge_group: + push: + branches: + - main + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Run verification checks + run: make verify + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Run golangci-lint + run: make lint diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..c932bd4 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,31 @@ +name: "Close stale issues and PRs" + +on: + schedule: + - cron: "0 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 90 + days-before-close: 30 + stale-issue-label: "lifecycle/stale" + stale-pr-label: "lifecycle/stale" + stale-issue-message: > + Issues go stale after 90 days of inactivity. If there is no further + activity, the issue will be closed in another 30 days. + stale-pr-message: > + PRs go stale after 90 days of inactivity. If there is no further + activity, the PR will be closed in another 30 days. + close-issue-message: "This issue has been closed due to inactivity." + close-pr-message: "This pull request has been closed due to inactivity." + exempt-issue-labels: "security,planned,priority/critical,lifecycle/frozen,verified" + exempt-pr-labels: "security,planned,priority/critical,lifecycle/frozen,verified" + operations-per-run: 30 diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml new file mode 100644 index 0000000..8d9e6c1 --- /dev/null +++ b/.github/workflows/unit-test.yaml @@ -0,0 +1,20 @@ +name: unit-test + +on: + workflow_dispatch: + pull_request: + merge_group: + push: + branches: + - main + +jobs: + unit-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Run unit tests + run: make test diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..548c12c --- /dev/null +++ b/OWNERS @@ -0,0 +1,18 @@ +approvers: + - grokspawn + - joelanford + - kevinrizza + - pedjak + - perdasilva + - tmshort + +reviewers: + - ankitathomas + - dtfranz + - fgiudici + - grokspawn + - joelanford + - pedjak + - perdasilva + - rashmigottipati + - tmshort diff --git a/hack/tools/check-go-version.sh b/hack/tools/check-go-version.sh new file mode 100755 index 0000000..fb9bc8f --- /dev/null +++ b/hack/tools/check-go-version.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +U_FLAG='false' +B_FLAG='' + +usage() { + cat <] [-h] [-u] + +Reports on golang mod file version updates, returns an error when a go.mod +file exceeds the root go.mod file (used as a threshold). + +Options: + -b git reference (branch or SHA) to use as a baseline. + Defaults to 'main'. + -h Help (this text). + -u Error on any update, even below the threshold. +EOF +} + +while getopts 'b:hu' f; do + case "${f}" in + b) B_FLAG="${OPTARG}" ;; + h) usage + exit 0 ;; + u) U_FLAG='true' ;; + *) echo "Unknown flag ${f}" + usage + exit 1 ;; + esac +done + +BASE_REF=${B_FLAG:-main} +ROOT_GO_MOD="./go.mod" +GO_VER=$(sed -En 's/^go (.*)$/\1/p' "${ROOT_GO_MOD}") +OLDIFS="${IFS}" +IFS='.' MAX_VER=(${GO_VER}) +IFS="${OLDIFS}" + +if [ ${#MAX_VER[*]} -ne 3 -a ${#MAX_VER[*]} -ne 2 ]; then + echo "Invalid go version: ${GO_VER}" + exit 1 +fi + +GO_MAJOR=${MAX_VER[0]} +GO_MINOR=${MAX_VER[1]} +GO_PATCH=${MAX_VER[2]} + +RETCODE=0 + +check_version () { + local whole=$1 + local file=$2 + OLDIFS="${IFS}" + IFS='.' ver=(${whole}) + IFS="${OLDIFS}" + + if [ ${ver[0]} -gt ${GO_MAJOR} ]; then + echo "${file}: ${whole}: Bad golang version (expected ${GO_VER} or less)" + return 1 + fi + if [ ${ver[1]} -gt ${GO_MINOR} ]; then + echo "${file}: ${whole}: Bad golang version (expected ${GO_VER} or less)" + return 1 + fi + + if [ ${#ver[*]} -eq 2 ] ; then + return 0 + fi + if [ ${#ver[*]} -ne 3 ] ; then + echo "${file}: ${whole}: Badly formatted golang version" + return 1 + fi + + if [ ${ver[1]} -eq ${GO_MINOR} -a ${ver[2]} -gt ${GO_PATCH} ]; then + echo "${file}: ${whole}: Bad golang version (expected ${GO_VER} or less)" + return 1 + fi + return 0 +} + +echo "Found golang version: ${GO_VER}" + +for f in $(find . -name "*.mod"); do + v=$(sed -En 's/^go (.*)$/\1/p' ${f}) + if [ -z ${v} ]; then + echo "${f}: Skipping, no version found" + continue + fi + if ! check_version ${v} ${f}; then + RETCODE=1 + fi + old=$(git grep -ohP '^go .*$' "${BASE_REF}" -- "${f}") + old=${old#go } + new=$(git grep -ohP '^go .*$' "${f}") + new=${new#go } + # If ${old} is empty, it means this is a new .mod file + if [ -z "${old}" ]; then + continue + fi + # Check if patch version remains 0: X.x.0 <-> X.x + if [ "${new}.0" == "${old}" -o "${new}" == "${old}.0" ]; then + continue + fi + if [ "${new}" != "${old}" ]; then + # We NEED to report on changes in the root go.mod, regardless of the U_FLAG + if [ "${f}" == "${ROOT_GO_MOD}" ]; then + echo "${f}: ${v}: Updated ROOT golang version from ${old}" + RETCODE=1 + continue + fi + if ${U_FLAG}; then + echo "${f}: ${v}: Updated golang version from ${old}" + RETCODE=1 + fi + fi +done + +exit ${RETCODE} From 89a3c5b6548deac3292eb5830e447511d2e03a0c Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 13:37:48 -0400 Subject: [PATCH 07/22] Promote sigs.k8s.io/yaml to direct dependency types.go imports sigs.k8s.io/yaml directly; go mod tidy correctly moves it from the indirect block to the direct require block. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8d6dfcb..1733efe 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 sigs.k8s.io/controller-runtime v0.24.1 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -61,5 +62,4 @@ require ( 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 - sigs.k8s.io/yaml v1.6.0 // indirect ) From 11ea2ced3a6dc97976423b124ee3f943bf182e7a Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 14:42:01 -0400 Subject: [PATCH 08/22] Add R2.7 adaptation note for ClusterObjectDeployment (boxcutter phase 2) REQUIREMENTS.md R2.7 states the implementation must track upcoming boxcutter changes that may introduce ClusterObjectDeployment and be prepared to adapt. Adds a TODO comment on CreateClusterObjectSet pointing at OPRUN-4716 and the boxcutter design so future maintainers know where the adaptation point is. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/migration.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index fd086d5..d44f1e8 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -253,6 +253,11 @@ func (m *Migrator) RecoverBeforeCE(ctx context.Context, opts Options, backup *Ba // CreateClusterObjectSet builds and creates a COS from the collected resources. // It uses CollisionProtection=IfNoController so OLMv1 can adopt existing resources (including CRDs). // The COS is annotated with the source Subscription reference. +// +// TODO(R2.7): when boxcutter phase 2 introduces ClusterObjectDeployment as a replacement or +// complement to ClusterObjectSet, update this function (and its callers) to create whichever +// OLMv1 revision object(s) are appropriate. Track upstream progress at OPRUN-4716 and the +// boxcutter ClusterObjectDeployment design. func (m *Migrator) CreateClusterObjectSet(ctx context.Context, opts Options, info *MigrationInfo) error { cosName := fmt.Sprintf("%s-1", opts.ClusterExtensionName) From a88536e45435adc10c5fa3ebce71363adbca0e97 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 14:47:13 -0400 Subject: [PATCH 09/22] Implement SecretPacker for large-bundle support (R2.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R2.4 requires collected objects to be stored via Secret-backed refs rather than inline in the COS spec, so that large bundles do not hit Kubernetes etcd's 1.5 MiB size limit. secretpacker.go (ported from operator-controller internal): - Gzip-compresses objects exceeding 900 KiB before storage - Splits across multiple Secrets when a batch would exceed 900 KiB - Content-addressed Secret keys (SHA-256, deduplicates identical objects) - Immutable Secrets with revision-name and owner-name labels migration.go / CreateClusterObjectSet: - Builds inline COS phases first (PhaseSort groups by kind) - Calls secretPacker.pack() to produce ref Secrets + pos→ref mapping - Creates each Secret in opts.SystemNamespace before the COS - Replaces inline objects with ObjectSourceRef entries in the phases types.go: - Adds SystemNamespace field to Options (default: "olmv1-system") - Adds systemNamespace() helper method go.mod: k8s.io/utils promoted to direct (used by ptr.To in secretpacker). Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- go.mod | 2 +- migration/pkg/migration/migration.go | 34 +++++ migration/pkg/migration/secretpacker.go | 182 ++++++++++++++++++++++++ migration/pkg/migration/types.go | 12 ++ 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 migration/pkg/migration/secretpacker.go diff --git a/go.mod b/go.mod index 1733efe..0ba77b3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( k8s.io/apiextensions-apiserver v0.36.2 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 + k8s.io/utils v0.0.0-20260626114624-be93311217bd sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/yaml v1.6.0 ) @@ -58,7 +59,6 @@ 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/utils v0.0.0-20260626114624-be93311217bd // 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 diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index d44f1e8..ef3fa43 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -260,6 +260,7 @@ func (m *Migrator) RecoverBeforeCE(ctx context.Context, opts Options, backup *Ba // boxcutter ClusterObjectDeployment design. func (m *Migrator) CreateClusterObjectSet(ctx context.Context, opts Options, info *MigrationInfo) error { cosName := fmt.Sprintf("%s-1", opts.ClusterExtensionName) + systemNS := opts.systemNamespace() cosObjects := make([]ocv1ac.ClusterObjectSetObjectApplyConfiguration, 0, len(info.CollectedObjects)) for _, obj := range info.CollectedObjects { @@ -271,6 +272,39 @@ func (m *Migrator) CreateClusterObjectSet(ctx context.Context, opts Options, inf phases := PhaseSort(cosObjects) + // Pack inline objects into Secrets to stay within etcd's size limit (R2.4). + // SecretPacker gzip-compresses large objects and splits across multiple Secrets + // when the combined size would exceed 900 KiB per Secret. + packer := &secretPacker{ + RevisionName: cosName, + OwnerName: opts.ClusterExtensionName, + SystemNamespace: systemNS, + } + packed, err := packer.pack(phases) + if err != nil { + return fmt.Errorf("failed to pack COS objects into Secrets: %w", err) + } + + // Create ref Secrets before the COS so the COS controller can find them immediately. + for i := range packed.Secrets { + secret := &packed.Secrets[i] + if err := m.Client.Create(ctx, secret); err != nil { + return fmt.Errorf("failed to create COS ref Secret %s: %w", secret.Name, err) + } + } + + // Replace inline objects with Secret refs in the phases. + for pos, ref := range packed.Refs { + phaseIdx, objIdx := pos[0], pos[1] + localRef := ref + phases[phaseIdx].Objects[objIdx].Object = nil + phases[phaseIdx].Objects[objIdx].Ref = &ocv1ac.ObjectSourceRefApplyConfiguration{ + Name: &localRef.Name, + Namespace: &localRef.Namespace, + Key: &localRef.Key, + } + } + cosSpec := ocv1ac.ClusterObjectSetSpec(). WithRevision(1). WithCollisionProtection(ocv1.CollisionProtectionIfNoController). diff --git a/migration/pkg/migration/secretpacker.go b/migration/pkg/migration/secretpacker.go new file mode 100644 index 0000000..edf47e8 --- /dev/null +++ b/migration/pkg/migration/secretpacker.go @@ -0,0 +1,182 @@ +package migration + +// SecretPacker packs serialized objects from COS phases into one or more immutable +// Secrets so that large bundles do not exceed Kubernetes etcd's size limit. +// +// Ported from operator-controller internal/operator-controller/applier/secretpacker.go. +// Objects are gzip-compressed when they exceed gzipThreshold, and a new Secret is +// started whenever the current batch would exceed maxSecretDataSize. + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "sort" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + ocv1ac "github.com/operator-framework/operator-controller/applyconfigurations/api/v1" +) + +const ( + // maxSecretDataSize is the target maximum for Secret .data size before starting a + // new Secret. 900 KiB leaves headroom for base64 overhead within etcd's 1.5 MiB limit. + maxSecretDataSize = 900 * 1024 + + // gzipThreshold is the object size above which individual objects are compressed before storage. + gzipThreshold = 900 * 1024 +) + +// secretPacker packs serialized COS phase objects into immutable Secrets. +type secretPacker struct { + // RevisionName is the COS name — used to derive Secret names. + RevisionName string + // OwnerName is the CE name — recorded as a label on each Secret. + OwnerName string + // SystemNamespace is where Secrets are created (e.g. "olmv1-system"). + SystemNamespace string +} + +// packResult holds the packed Secrets and the ref entries that replace inline objects. +type packResult struct { + // Secrets to be created before the COS. + Secrets []corev1.Secret + // Refs maps (phaseIndex, objectIndex) to the ObjectSourceRef that replaces the inline object. + Refs map[[2]int]ocv1.ObjectSourceRef +} + +// pack takes COS phases with inline objects and produces: +// 1. A set of immutable Secrets containing the serialized objects. +// 2. A mapping from (phaseIdx, objIdx) to the corresponding ObjectSourceRef. +func (p *secretPacker) pack(phases []*ocv1ac.ClusterObjectSetPhaseApplyConfiguration) (*packResult, error) { + result := &packResult{ + Refs: make(map[[2]int]ocv1.ObjectSourceRef), + } + + type pendingRef struct { + pos [2]int + key string + } + + var ( + currentData = make(map[string][]byte) + currentSize int + currentPending []pendingRef + ) + + finalizeCurrent := func() { + if len(currentData) == 0 { + return + } + secret := p.newSecret(currentData) + for _, pr := range currentPending { + result.Refs[pr.pos] = ocv1.ObjectSourceRef{ + Name: secret.Name, + Namespace: p.SystemNamespace, + Key: pr.key, + } + } + result.Secrets = append(result.Secrets, secret) + currentData = make(map[string][]byte) + currentSize = 0 + currentPending = nil + } + + for phaseIdx, phase := range phases { + for objIdx, obj := range phase.Objects { + if obj.Object == nil { + continue // already a ref + } + + data, err := json.Marshal(obj.Object) + if err != nil { + return nil, fmt.Errorf("serializing object in phase %d index %d: %w", phaseIdx, objIdx, err) + } + + if len(data) > gzipThreshold { + compressed, cErr := gzipData(data) + if cErr != nil { + return nil, fmt.Errorf("compressing object in phase %d index %d: %w", phaseIdx, objIdx, cErr) + } + data = compressed + } + + if len(data) > maxSecretDataSize { + return nil, fmt.Errorf( + "object in phase %d index %d exceeds maximum Secret data size (%d bytes > %d bytes) even after compression", + phaseIdx, objIdx, len(data), maxSecretDataSize, + ) + } + + key := contentHash(data) + + if _, exists := currentData[key]; !exists { + if currentSize+len(data) > maxSecretDataSize && len(currentData) > 0 { + finalizeCurrent() + } + currentData[key] = data + currentSize += len(data) + } + currentPending = append(currentPending, pendingRef{pos: [2]int{phaseIdx, objIdx}, key: key}) + } + } + finalizeCurrent() + + return result, nil +} + +func (p *secretPacker) newSecret(data map[string][]byte) corev1.Secret { + return corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.secretNameFromData(data), + Namespace: p.SystemNamespace, + Labels: map[string]string{ + LabelRevisionName: p.RevisionName, + LabelOwnerName: p.OwnerName, + }, + }, + Immutable: ptr.To(true), + Type: corev1.SecretType(SecretTypeObjectData), //nolint:gosec // G101 false positive + Data: data, + } +} + +func (p *secretPacker) secretNameFromData(data map[string][]byte) string { + h := sha256.New() + keys := make([]string, 0, len(data)) + for k := range data { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + h.Write([]byte(k)) + h.Write(data[k]) + } + return fmt.Sprintf("%s-%x", p.RevisionName, h.Sum(nil)[:8]) +} + +func contentHash(data []byte) string { + h := sha256.Sum256(data) + return base64.RawURLEncoding.EncodeToString(h[:]) +} + +func gzipData(data []byte) ([]byte, error) { + var buf bytes.Buffer + w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) + if err != nil { + return nil, err + } + if _, err := w.Write(data); err != nil { + return nil, err + } + if err := w.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/migration/pkg/migration/types.go b/migration/pkg/migration/types.go index a4fbe09..60f5902 100644 --- a/migration/pkg/migration/types.go +++ b/migration/pkg/migration/types.go @@ -49,6 +49,18 @@ type Options struct { // DeleteOperatorGroup deletes the OperatorGroup when both this flag is set AND // no other Subscriptions remain in the namespace (R6). DeleteOperatorGroup bool + + // SystemNamespace is the namespace where COS ref Secrets are created (R2.4). + // Defaults to "olmv1-system" when empty. + SystemNamespace string +} + +// systemNamespace returns the effective system namespace. +func (o Options) systemNamespace() string { + if o.SystemNamespace != "" { + return o.SystemNamespace + } + return "olmv1-system" } // ApplyDefaults fills in default values for any unset optional fields. From e2497926522a805734631ffd8a7c6b6546101dd5 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 14:57:32 -0400 Subject: [PATCH 10/22] Fix C1: make spec.selector block soft with AcknowledgeWatchScopeChange REQUIREMENTS.md R3 C1 says the watch-scope check is soft and overridable via --acknowledge-watch-scope-change. The OperatorGroup spec.selector check was hard-blocking; it now follows the same override path as spec.targetNamespaces. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/compatibility.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go index 1d6dbb4..fc5a842 100644 --- a/migration/pkg/migration/compatibility.go +++ b/migration/pkg/migration/compatibility.go @@ -80,13 +80,21 @@ func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([] }) } - // spec.selector + // spec.selector — scoped namespace selector means not AllNamespaces (C1 — soft) if og.Spec.Selector != nil && !isEmptyLabelSelector(og.Spec.Selector) { - checks = append(checks, CheckResult{ - Name: "No namespace selector", - Passed: false, - Message: "OperatorGroup has spec.selector set; must convert to spec.targetNamespaces before migration", - }) + if opts.AcknowledgeWatchScopeChange { + checks = append(checks, CheckResult{ + Name: "No namespace selector", + Passed: true, + Message: "overridden: operator will run AllNamespaces post-migration (watch scope change acknowledged)", + }) + } else { + checks = append(checks, CheckResult{ + Name: "No namespace selector", + Passed: false, + Message: "OperatorGroup has spec.selector set; OLMv1 uses AllNamespaces — pass --acknowledge-watch-scope-change to override", + }) + } } else { checks = append(checks, CheckResult{ Name: "No namespace selector", From d13af8910979e0a6086052c108996f0e897bad09 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:03:06 -0400 Subject: [PATCH 11/22] Fix C8: gate Subscription state check on AcknowledgeNotSteadyState R3 C8 covers both CSV health and Subscription state as a single soft check overridable via --acknowledge-not-steady-state. The Subscription state check was hard-blocking; it now respects the flag, matching the CSV phase/reason check on lines 113 and 127. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/readiness.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/migration/pkg/migration/readiness.go b/migration/pkg/migration/readiness.go index e8cef55..9c696d9 100644 --- a/migration/pkg/migration/readiness.go +++ b/migration/pkg/migration/readiness.go @@ -22,7 +22,7 @@ func (m *Migrator) CheckReadiness(ctx context.Context, opts Options) (*PreMigrat return nil, fmt.Errorf("failed to get Subscription %s/%s: %w", opts.SubscriptionNamespace, opts.SubscriptionName, err) } - // Subscription state + // Subscription state (C8 — soft; same flag as CSV health) if sub.Status.State == operatorsv1alpha1.SubscriptionStateAtLatest || sub.Status.State == operatorsv1alpha1.SubscriptionStateUpgradePending { report.Checks = append(report.Checks, CheckResult{ @@ -30,11 +30,17 @@ func (m *Migrator) CheckReadiness(ctx context.Context, opts Options) (*PreMigrat Passed: true, Message: fmt.Sprintf("state is %q", sub.Status.State), }) + } else if opts.AcknowledgeNotSteadyState { + report.Checks = append(report.Checks, CheckResult{ + Name: "Subscription state", + Passed: true, + Message: fmt.Sprintf("overridden: Subscription state is %q (not steady state acknowledged)", sub.Status.State), + }) } else { report.Checks = append(report.Checks, CheckResult{ Name: "Subscription state", Passed: false, - Message: fmt.Sprintf("must be %q or %q, got %q", operatorsv1alpha1.SubscriptionStateAtLatest, operatorsv1alpha1.SubscriptionStateUpgradePending, sub.Status.State), + Message: fmt.Sprintf("must be %q or %q, got %q; pass --acknowledge-not-steady-state to override", operatorsv1alpha1.SubscriptionStateAtLatest, operatorsv1alpha1.SubscriptionStateUpgradePending, sub.Status.State), }) } From 27ca37b590e5e98117264158ab413c036f6d43ae Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:07:06 -0400 Subject: [PATCH 12/22] Implement C5: OLMv0-API RBAC check without OLMv1 equivalent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R3 C5 flags operators whose CSV clusterPermissions grant access to operators.coreos.com resources (subscriptions, installplans, clusterserviceversions, catalogsources — operatorconditions excluded per spec) without also granting OLMv1 API access (olm.operatorframework.io group). Operators updated for dual compatibility carry both sets and pass. AcknowledgeOLMv0APIAccess (--acknowledge-olmv0-api-access) overrides the soft block. The check reads CSV.Spec.InstallStrategy.StrategySpec .ClusterPermissions so no live cluster RBAC queries are needed — the bundle manifest is the authoritative source. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/compatibility.go | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go index fc5a842..c24d813 100644 --- a/migration/pkg/migration/compatibility.go +++ b/migration/pkg/migration/compatibility.go @@ -38,6 +38,9 @@ func (m *Migrator) CheckCompatibility(ctx context.Context, opts Options, csv *op } report.Checks = append(report.Checks, condCheck) + // OLMv0-API RBAC check (C5 — soft) + report.Checks = append(report.Checks, checkOLMv0APIAccess(opts, csv)) + return report, nil } @@ -237,6 +240,71 @@ func checkNoDependencies(propertiesJSON string) []CheckResult { return issues } +// olmv0APIResources is the set of operators.coreos.com resource names that signal OLMv0-API +// dependency (per R3 C5). operatorconditions is explicitly excluded — OLMv0 stamps that RBAC +// on every operator and its presence is not a usage signal. +var olmv0APIResources = map[string]bool{ + "subscriptions": true, + "installplans": true, + "clusterserviceversions": true, + "catalogsources": true, +} + +// checkOLMv0APIAccess implements C5: flag operators whose clusterPermissions grant access to +// OLMv0 APIs (operators.coreos.com, excluding operatorconditions) without also granting +// equivalent OLMv1 API access (olm.operatorframework.io). Operators updated for OLMv1 +// compatibility carry both sets of permissions and pass this check. +func checkOLMv0APIAccess(opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) CheckResult { + hasOLMv0Access := false + hasOLMv1Access := false + + for _, perm := range csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions { + for _, rule := range perm.Rules { + for _, group := range rule.APIGroups { + switch group { + case "operators.coreos.com": + for _, res := range rule.Resources { + if olmv0APIResources[res] || res == "*" { + hasOLMv0Access = true + } + } + case "olm.operatorframework.io": + hasOLMv1Access = true + } + } + } + } + + if !hasOLMv0Access { + return CheckResult{ + Name: "OLMv0-API RBAC", + Passed: true, + Message: "CSV clusterPermissions do not grant OLMv0 API access", + } + } + if hasOLMv1Access { + return CheckResult{ + Name: "OLMv0-API RBAC", + Passed: true, + Message: "CSV clusterPermissions grant both OLMv0 and OLMv1 API access (updated for compatibility)", + } + } + + // OLMv0 access without OLMv1 access — soft block + if opts.AcknowledgeOLMv0APIAccess { + return CheckResult{ + Name: "OLMv0-API RBAC", + Passed: true, + Message: "overridden: OLMv0 API RBAC without OLMv1 equivalent (olmv0-api-access acknowledged)", + } + } + return CheckResult{ + Name: "OLMv0-API RBAC", + Passed: false, + Message: "CSV clusterPermissions grant operators.coreos.com access without OLMv1 equivalent; pass --acknowledge-olmv0-api-access to override", + } +} + // checkNoOperatorConditions enforces C4 — no active OperatorCondition status entries. // RBAC presence alone is NOT treated as usage; only status.conditions entries count. func (m *Migrator) checkNoOperatorConditions(ctx context.Context, opts Options, csv *operatorsv1alpha1.ClusterServiceVersion) (CheckResult, error) { From 6c8ecb3d508d86ea699ecdfff3e6b71f313a9031 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:17:07 -0400 Subject: [PATCH 13/22] Fix R4: fall back to status.install when installPlanRef is absent R4 says status.installPlanRef should be used for supplementary resource collection with a fallback to the deprecated status.install field. When installPlanRef is nil, GetCSVAndInstallPlan now checks sub.Status.Install (the legacy InstallPlanReference) before giving up on InstallPlan lookup. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/collector.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/migration/pkg/migration/collector.go b/migration/pkg/migration/collector.go index 0d516f7..1b2028a 100644 --- a/migration/pkg/migration/collector.go +++ b/migration/pkg/migration/collector.go @@ -90,13 +90,23 @@ func (m *Migrator) GetCSVAndInstallPlan(ctx context.Context, opts Options) (*ope } var ip *operatorsv1alpha1.InstallPlan + ipName, ipNamespace := "", sub.Namespace if sub.Status.InstallPlanRef != nil { + ipName = sub.Status.InstallPlanRef.Name + if sub.Status.InstallPlanRef.Namespace != "" { + ipNamespace = sub.Status.InstallPlanRef.Namespace + } + } else if sub.Status.Install != nil { + // Fallback to the deprecated status.install field (R4). + ipName = sub.Status.Install.Name + } + if ipName != "" { ip = &operatorsv1alpha1.InstallPlan{} if err := m.Client.Get(ctx, types.NamespacedName{ - Name: sub.Status.InstallPlanRef.Name, - Namespace: sub.Status.InstallPlanRef.Namespace, + Name: ipName, + Namespace: ipNamespace, }, ip); err != nil { - return nil, nil, nil, fmt.Errorf("failed to get InstallPlan %s: %w", sub.Status.InstallPlanRef.Name, err) + return nil, nil, nil, fmt.Errorf("failed to get InstallPlan %s: %w", ipName, err) } } From 52a177a00247e3e6ba76170a19ee4fd1187083f6 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:20:08 -0400 Subject: [PATCH 14/22] Fix R4: resolve defaultChannel from ClusterCatalog when spec.channel is empty When a Subscription has no spec.channel, OLMv0 resolved the package's declared defaultChannel from the CatalogSource. OLMv1 with no channels considers upgrade edges across all channels, which may differ from what OLMv0 delivered. Changes: - catalogMeta gains DefaultChannel field from the olm.package FBC schema - CatalogPackageInfo.DefaultChannel populated by parseCatalogResponse - CreateClusterExtension: when info.Channel == "", queries the resolved ClusterCatalog via QueryCatalogForPackage to get DefaultChannel and sets it explicitly on CE.spec.source.catalog.channels - Warns via progress if the default channel cannot be determined, so the admin knows to verify upgrade behavior post-migration Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/catalog.go | 15 ++++++++++----- migration/pkg/migration/migration.go | 21 +++++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/migration/pkg/migration/catalog.go b/migration/pkg/migration/catalog.go index 30e6a3d..8aed234 100644 --- a/migration/pkg/migration/catalog.go +++ b/migration/pkg/migration/catalog.go @@ -20,11 +20,12 @@ import ( // catalogMeta represents a single entry from the catalog JSONL response. type catalogMeta struct { - Schema string `json:"schema"` - Name string `json:"name"` - Package string `json:"package"` - Props json.RawMessage `json:"properties,omitempty"` - Entries []channelEntry `json:"entries,omitempty"` + Schema string `json:"schema"` + Name string `json:"name"` + Package string `json:"package"` + DefaultChannel string `json:"defaultChannel,omitempty"` + Props json.RawMessage `json:"properties,omitempty"` + Entries []channelEntry `json:"entries,omitempty"` } type channelEntry struct { @@ -34,6 +35,7 @@ type channelEntry struct { // CatalogPackageInfo holds the results of querying a catalog for a package. type CatalogPackageInfo struct { Found bool + DefaultChannel string // the package's declared defaultChannel from the FBC AvailableVersions []string AvailableChannels []string VersionFound bool @@ -98,6 +100,9 @@ func parseCatalogResponse(body io.Reader, packageName, version, channel string) case "olm.package": if meta.Name == packageName { info.Found = true + if meta.DefaultChannel != "" { + info.DefaultChannel = meta.DefaultChannel + } } case "olm.bundle": if meta.Package != packageName { diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index ef3fa43..a619804 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -421,8 +421,25 @@ func (m *Migrator) CreateClusterExtension(ctx context.Context, opts Options, inf ce.Spec.Source.Catalog.Version = info.Version } - if info.Channel != "" { - ce.Spec.Source.Catalog.Channels = []string{info.Channel} + // R4: when spec.channel is empty, OLMv0 resolved the defaultChannel from the catalog. + // OLMv1 without channels considers upgrade edges across *all* channels, which may differ. + // Query the resolved ClusterCatalog for the package's declared defaultChannel and set it + // explicitly. Warn if it cannot be determined (R4 spec requirement). + channel := info.Channel + if channel == "" && info.ResolvedCatalogName != "" && m.RESTConfig != nil { + var catalog ocv1.ClusterCatalog + if err := m.Client.Get(ctx, client.ObjectKey{Name: info.ResolvedCatalogName}, &catalog); err == nil { + pkgInfo, qErr := m.QueryCatalogForPackage(ctx, &catalog, info.PackageName, "", "", m.RESTConfig) + if qErr == nil && pkgInfo.DefaultChannel != "" { + channel = pkgInfo.DefaultChannel + m.progress(fmt.Sprintf("Resolved default channel %q for package %q from ClusterCatalog %s", channel, info.PackageName, info.ResolvedCatalogName)) + } else { + m.progress(fmt.Sprintf("Warning: could not determine defaultChannel for package %q — CE will consider all channels; verify upgrade behavior post-migration", info.PackageName)) + } + } + } + if channel != "" { + ce.Spec.Source.Catalog.Channels = []string{channel} } if info.ResolvedCatalogName != "" { From 4a2c42857e90f05062d5bf3e1bf9bae3c74d26b6 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:27:23 -0400 Subject: [PATCH 15/22] Fix R4: map spec.config to CE.spec.config.inline.deploymentConfig R4 requires Subscription spec.config (SubscriptionConfig) to map 1:1 to CE.spec.config.inline.deploymentConfig. All sub-fields (env, envFrom, volumes, volumeMounts, tolerations, resources, nodeSelector, affinity, annotations) are carried forward. spec.config.selector is dropped with a warning (never honored in OLMv0; no CE equivalent per R4 and R7). Changes: - MigrationInfo gains SubscriptionConfig *SubscriptionConfig field - GetBundleInfo populates it from sub.Spec.Config, drops selector, and warns via the progress function if selector was set - CreateClusterExtension serializes SubscriptionConfig as {"deploymentConfig":} and sets it on CE.spec.config (configType: Inline) when non-nil - k8s.io/apiextensions-apiserver added as import for apiextensionsv1.JSON Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/catalog.go | 2 +- migration/pkg/migration/collector.go | 11 +++++++++++ migration/pkg/migration/migration.go | 19 +++++++++++++++++++ migration/pkg/migration/types.go | 4 ++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/migration/pkg/migration/catalog.go b/migration/pkg/migration/catalog.go index 8aed234..7836dca 100644 --- a/migration/pkg/migration/catalog.go +++ b/migration/pkg/migration/catalog.go @@ -35,7 +35,7 @@ type channelEntry struct { // CatalogPackageInfo holds the results of querying a catalog for a package. type CatalogPackageInfo struct { Found bool - DefaultChannel string // the package's declared defaultChannel from the FBC + DefaultChannel string // the package's declared defaultChannel from the FBC AvailableVersions []string AvailableChannels []string VersionFound bool diff --git a/migration/pkg/migration/collector.go b/migration/pkg/migration/collector.go index 1b2028a..450f1bf 100644 --- a/migration/pkg/migration/collector.go +++ b/migration/pkg/migration/collector.go @@ -136,6 +136,17 @@ func (m *Migrator) GetBundleInfo(ctx context.Context, opts Options, csv *operato info.BundleName = csv.Name info.Version = parseCSVVersion(csv) + // R4: spec.config → CE deploymentConfig. Drop spec.config.selector (never honored in + // OLMv0; no CE equivalent) and warn if it was set. + if sub.Spec.Config != nil { + cfg := sub.Spec.Config.DeepCopy() + if cfg.Selector != nil { + m.progress("Warning: Subscription spec.config.selector is not supported by OLMv1 and will be dropped during migration") + cfg.Selector = nil + } + info.SubscriptionConfig = cfg + } + if ip != nil { for _, bl := range ip.Status.BundleLookups { if bl.Identifier == csv.Name { diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index a619804..d51bdec 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -7,6 +7,7 @@ import ( "strings" "time" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" @@ -450,6 +451,24 @@ func (m *Migrator) CreateClusterExtension(ctx context.Context, opts Options, inf } } + // R4: map spec.config → CE.spec.config.inline.deploymentConfig (R4, R7). + // DeploymentConfig is a type alias of SubscriptionConfig in operator-controller; + // the JSON key must be "deploymentConfig" per the bundle config schema. + if info.SubscriptionConfig != nil { + cfgJSON, err := json.Marshal(info.SubscriptionConfig) + if err != nil { + return fmt.Errorf("failed to marshal SubscriptionConfig for CE: %w", err) + } + inlineJSON, err := json.Marshal(map[string]json.RawMessage{"deploymentConfig": cfgJSON}) + if err != nil { + return fmt.Errorf("failed to marshal CE inline config: %w", err) + } + ce.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{Raw: inlineJSON}, + } + } + if err := m.Client.Create(ctx, ce); err != nil { return fmt.Errorf("failed to create ClusterExtension: %w", err) } diff --git a/migration/pkg/migration/types.go b/migration/pkg/migration/types.go index 60f5902..b80f55f 100644 --- a/migration/pkg/migration/types.go +++ b/migration/pkg/migration/types.go @@ -86,6 +86,10 @@ type MigrationInfo struct { ResolvedCatalogName string CollectedObjects []unstructured.Unstructured + // SubscriptionConfig holds spec.config from the Subscription for mapping to CE (R4). + // spec.config.selector is dropped (never honored in OLMv0; no CE equivalent). + SubscriptionConfig *operatorsv1alpha1.SubscriptionConfig + // Subscription spec JSON for the CE migration-subscription-backup annotation (R2.5). SubscriptionBackupJSON string // OperatorGroup spec JSON for the CE migration-operatorgroup-backup annotation (R2.5). From 976b64a60f2ac623f36fab6f07497149fd554337 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:39:16 -0400 Subject: [PATCH 16/22] Fix R5: dedup key uses Group/Kind/namespace/name, not APIVersion resourceKey() was including APIVersion in the deduplication key. The same resource can be returned from different collection sources with different version strings (e.g. "v1" vs "core/v1" from the Operator CR refs vs label queries), causing missed deduplication. R5 specifies deduplication by GVK+namespace+name. Group and Kind are sufficient to identify the resource type stably; APIVersion is redundant and version-alias-sensitive. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/collector.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/migration/pkg/migration/collector.go b/migration/pkg/migration/collector.go index 450f1bf..83fd73f 100644 --- a/migration/pkg/migration/collector.go +++ b/migration/pkg/migration/collector.go @@ -254,12 +254,17 @@ func (m *Migrator) CollectResources(ctx context.Context, opts Options, csv *oper return collected, nil } +// resourceKey produces a dedup key that is stable across API version aliases. +// APIVersion is intentionally excluded: the same resource may be returned from +// different collection sources using different version strings (e.g. "v1" vs +// "core/v1"), and including it would prevent correct deduplication (R5). func resourceKey(obj unstructured.Unstructured) string { + gvk := obj.GetObjectKind().GroupVersionKind() return fmt.Sprintf("%s/%s/%s/%s", - obj.GetObjectKind().GroupVersionKind().GroupKind().String(), + gvk.Group, + gvk.Kind, obj.GetNamespace(), - obj.GetName(), - obj.GetAPIVersion()) + obj.GetName()) } func (m *Migrator) getCRDsByPackage(ctx context.Context, opts Options, packageName string) ([]unstructured.Unstructured, error) { From a91e54a6faf4c68575be63d7829af95682c10748 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:40:50 -0400 Subject: [PATCH 17/22] Fix R5: olm.owner label query does not require olm.managed=true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R5 says supplementary source 1 matches all resources of eligible kinds with olm.owner=. The extra olm.managed=true requirement was not in the spec and narrowed the query unnecessarily — resources that carry olm.owner but were not stamped with olm.managed would be missed. Co-Authored-By: Claude Sonnet 4.6 (1M context) Signed-off-by: Todd Short --- migration/pkg/migration/collector.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migration/pkg/migration/collector.go b/migration/pkg/migration/collector.go index 83fd73f..b983951 100644 --- a/migration/pkg/migration/collector.go +++ b/migration/pkg/migration/collector.go @@ -299,11 +299,11 @@ func (m *Migrator) gatherResourcesByOwnerLabel(ctx context.Context, csvName stri Kind: gvk.Kind + "List", }) + // R5: match on olm.owner= only. The spec does not require + // olm.managed=true; adding it would miss resources that carry olm.owner + // but were not stamped with the managed label. if err := m.Client.List(ctx, &list, - client.MatchingLabels{ - "olm.managed": "true", - "olm.owner": csvName, - }, + client.MatchingLabels{"olm.owner": csvName}, ); err != nil { continue } From 46d2604e3be2d4757068a542bd60f7cc5f7d0486 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:43:40 -0400 Subject: [PATCH 18/22] Fix R6: TechPreviewUnsafeFailForward is informational warning, not a block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R6 says spec.upgradeStrategy = TechPreviewUnsafeFailForward is not mapped to OLMv1 (not equivalent to SelfCertified) and should produce an informational warning only — not block migration. The check was emitting Passed: false, hard-blocking operators that use this strategy. Now emits Passed: true with a note that the strategy will be ignored post-migration. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migration/pkg/migration/compatibility.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migration/pkg/migration/compatibility.go b/migration/pkg/migration/compatibility.go index c24d813..8549301 100644 --- a/migration/pkg/migration/compatibility.go +++ b/migration/pkg/migration/compatibility.go @@ -106,12 +106,13 @@ func (m *Migrator) checkAllNamespacesMode(ctx context.Context, opts Options) ([] }) } - // spec.upgradeStrategy + // spec.upgradeStrategy — TechPreviewUnsafeFailForward is not mapped to OLMv1 and is not + // equivalent to SelfCertified. R6 says this is informational only: warn but do not block. if og.Spec.UpgradeStrategy != "" && og.Spec.UpgradeStrategy != operatorsv1.UpgradeStrategyDefault { checks = append(checks, CheckResult{ Name: "Upgrade strategy", - Passed: false, - Message: fmt.Sprintf("must be %q or unset, got %q", operatorsv1.UpgradeStrategyDefault, og.Spec.UpgradeStrategy), + Passed: true, + Message: fmt.Sprintf("OperatorGroup upgradeStrategy %q is not mapped to OLMv1 and will be ignored post-migration", og.Spec.UpgradeStrategy), }) } else { checks = append(checks, CheckResult{ From 2c9868dc514806ca4c88af05fe6e8f5e7c06cfcd Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:54:09 -0400 Subject: [PATCH 19/22] Fix R8: note spec.secrets and spec.grpcPodConfig when present R8 says both fields should be reported as informational notes since they have no OLMv1 equivalent: - spec.secrets: OLMv1 uses the cluster global pull secret - spec.grpcPodConfig: catalogd manages the serving pod Adds Notes []string to CatalogMigrationResult. The notes are populated per-CatalogSource and included on all result types (created, adopted, dry-run, error) so callers can surface them regardless of outcome. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../pkg/catalogmigration/catalogmigration.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/migration/pkg/catalogmigration/catalogmigration.go b/migration/pkg/catalogmigration/catalogmigration.go index 3e13628..0b4e4ec 100644 --- a/migration/pkg/catalogmigration/catalogmigration.go +++ b/migration/pkg/catalogmigration/catalogmigration.go @@ -38,6 +38,7 @@ type CatalogMigrationResult struct { ClusterCatalogName string Status string // "created", "adopted", "skipped", "error", "dry-run" Reason string + Notes []string // informational notices (e.g. dropped fields with no OLMv1 equivalent) } // CatalogMigrator migrates OLMv0 CatalogSources to OLMv1 ClusterCatalogs. @@ -181,6 +182,15 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr // Convert poll interval pollMinutes := convertPollInterval(cs) + // Collect informational notes for fields that have no OLMv1 equivalent (R8). + var notes []string + if len(cs.Spec.Secrets) > 0 { + notes = append(notes, fmt.Sprintf("spec.secrets (%d secret(s)) has no OLMv1 equivalent; OLMv1 uses the cluster global pull secret", len(cs.Spec.Secrets))) + } + if cs.Spec.GrpcPodConfig != nil { + notes = append(notes, "spec.grpcPodConfig has no OLMv1 equivalent; catalogd manages the serving pod configuration") + } + csRef := fmt.Sprintf("%s/%s", cs.Namespace, cs.Name) // Check if already created this run (consolidation case) @@ -191,6 +201,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: ccName, Status: "adopted", Reason: fmt.Sprintf("consolidated into shared ClusterCatalog %s", ccName), + Notes: notes, }) continue } @@ -205,6 +216,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: existing.Name, Status: "dry-run", Reason: fmt.Sprintf("would adopt existing ClusterCatalog %s", existing.Name), + Notes: notes, }) continue } @@ -216,6 +228,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: existing.Name, Status: "error", Reason: fmt.Sprintf("failed to annotate existing ClusterCatalog: %v", err), + Notes: notes, }) continue } @@ -227,6 +240,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: existing.Name, Status: "adopted", Reason: "existing ClusterCatalog with matching image adopted", + Notes: notes, }) // Handle --delete-catalogsource @@ -244,6 +258,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: ccName, Status: "dry-run", Reason: fmt.Sprintf("would create ClusterCatalog %s from image %s", ccName, cs.Spec.Image), + Notes: notes, }) continue } @@ -277,6 +292,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: ccName, Status: "error", Reason: fmt.Sprintf("failed to create ClusterCatalog: %v", err), + Notes: notes, }) continue } @@ -289,6 +305,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: ccName, Status: "error", Reason: fmt.Sprintf("ClusterCatalog not serving: %v", err), + Notes: notes, }) continue } @@ -302,6 +319,7 @@ func (cm *CatalogMigrator) MigrateCatalogs(ctx context.Context, opts CatalogMigr ClusterCatalogName: ccName, Status: "created", Reason: fmt.Sprintf("created from image %s", cs.Spec.Image), + Notes: notes, }) // Handle --delete-catalogsource From 3e6e4849f1b3f97c960cd1156428b982beb0a544 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 15:54:58 -0400 Subject: [PATCH 20/22] Fix R8: only convert explicitly set poll intervals; leave absent ones unset convertPollInterval previously returned defaultPollMinutes (15) when UpdateStrategy was nil, writing pollIntervalMinutes=15 on ClusterCatalogs whose source CatalogSource had no explicit polling configured. R8 says only convert the explicitly set interval. When UpdateStrategy is absent, return 0 so pollIntervalMinutes is left unset and OLMv1 applies its own default. Also removes the now-unused defaultPollMinutes constant. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migration/pkg/catalogmigration/catalogmigration.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migration/pkg/catalogmigration/catalogmigration.go b/migration/pkg/catalogmigration/catalogmigration.go index 0b4e4ec..d751cce 100644 --- a/migration/pkg/catalogmigration/catalogmigration.go +++ b/migration/pkg/catalogmigration/catalogmigration.go @@ -20,8 +20,6 @@ import ( const ( // MigratedFromCatalogSourceAnnotation is set on ClusterCatalog when first created or adopted. MigratedFromCatalogSourceAnnotation = "olm.operatorframework.io/migrated-from-catalogsource" - - defaultPollMinutes = 15 ) // CatalogMigratorOptions configures the catalog migration. @@ -385,8 +383,11 @@ func convertPollInterval(cs operatorsv1alpha1.CatalogSource) int { return 0 } + // Only convert explicitly set values (R8). When no UpdateStrategy is configured, + // leave pollIntervalMinutes unset so OLMv1 uses its own default rather than + // inheriting the OLMv0 default (15m) which was never explicitly chosen. if cs.Spec.UpdateStrategy == nil || cs.Spec.UpdateStrategy.RegistryPoll == nil { - return defaultPollMinutes + return 0 } interval := cs.Spec.UpdateStrategy.Interval From 6f74bc350606e1da1ba453b7cf641260d3e1aa90 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 16:06:11 -0400 Subject: [PATCH 21/22] Fix R9: warn when migrating an operator that others depend on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R9 says migrating an operator that others depend on proceeds but must warn about dependents. Adds findDependents() which lists all installed CSVs and checks their operatorframework.io/properties annotations for olm.package.required entries matching the package being migrated. A warning is appended to OperatorScanResult.Warnings (new field) when dependents are found — eligibility is not affected. The warning fires from both ScanAllSubscriptions and ScanSubscription so callers surface it regardless of which scan path is used. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migration/pkg/migration/scan.go | 66 +++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/migration/pkg/migration/scan.go b/migration/pkg/migration/scan.go index e4ab3fb..8a51d89 100644 --- a/migration/pkg/migration/scan.go +++ b/migration/pkg/migration/scan.go @@ -22,8 +22,11 @@ type OperatorScanResult struct { Status OperatorStatus // four-state classification Reason string // human-readable explanation of the status (R1.3) Eligible bool // true when Status == Eligible (backwards compat) - Error error - FailedChecks []CheckResult + // Warnings are informational notices that do not affect eligibility (R9). + // Example: other installed operators declare a dependency on this package. + Warnings []string + Error error + FailedChecks []CheckResult } // ScanAllSubscriptions discovers all Subscriptions on the cluster, checks each for migration @@ -125,7 +128,7 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu // C7: catalog availability (hard check — no override). // Only run when readiness+compat pass to avoid noisy catalog errors for clearly ineligible operators. - if len(result.FailedChecks) == 0 { + if len(result.FailedChecks) == 0 { //nolint:nestif catalogName, catalogErr := m.ResolveClusterCatalog(ctx, &MigrationInfo{ PackageName: sub.Spec.Package, Channel: sub.Spec.Channel, @@ -149,6 +152,11 @@ func (m *Migrator) ScanAllSubscriptions(ctx context.Context) ([]OperatorScanResu result.Status = OperatorStatusEligible result.Reason = "passes all readiness, compatibility, and catalog-availability checks" result.Eligible = true + // R9: warn if other installed operators declare a dependency on this package. + if dependents := m.findDependents(ctx, sub.Spec.Package); len(dependents) > 0 { + result.Warnings = append(result.Warnings, + fmt.Sprintf("other operator(s) may depend on package %q: %v — verify they remain functional after migration", sub.Spec.Package, dependents)) + } } } else { result.Status = OperatorStatusIneligible @@ -257,6 +265,11 @@ func (m *Migrator) ScanSubscription(ctx context.Context, opts Options) (*Operato result.Status = OperatorStatusEligible result.Reason = "passes all readiness, compatibility, and catalog-availability checks" result.Eligible = true + // R9: warn if other installed operators declare a dependency on this package. + if dependents := m.findDependents(ctx, result.PackageName); len(dependents) > 0 { + result.Warnings = append(result.Warnings, + fmt.Sprintf("other operator(s) may depend on package %q: %v — verify they remain functional after migration", result.PackageName, dependents)) + } return result, nil } @@ -456,6 +469,53 @@ func (m *Migrator) Check(ctx context.Context, opts Options) (*OperatorScanResult return m.ScanSubscription(ctx, opts) } +// findDependents returns the names of installed operators (Subscription names) whose +// bundle properties declare an olm.package.required dependency on packageName (R9). +// The spec requires a warning — not a block — when migrating an operator others depend on. +func (m *Migrator) findDependents(ctx context.Context, packageName string) []string { + var subList operatorsv1alpha1.SubscriptionList + if err := m.Client.List(ctx, &subList); err != nil { + return nil + } + + var dependents []string + for _, sub := range subList.Items { + if sub.Spec.Package == packageName { + continue // skip the operator itself + } + if sub.Status.InstalledCSV == "" { + continue + } + var csv operatorsv1alpha1.ClusterServiceVersion + if err := m.Client.Get(ctx, client.ObjectKey{ + Name: sub.Status.InstalledCSV, + Namespace: sub.Namespace, + }, &csv); err != nil { + continue + } + propsJSON := csv.Annotations["operatorframework.io/properties"] + if propsJSON == "" { + continue + } + props, err := parseProperties(propsJSON) + if err != nil { + continue + } + for _, p := range props { + if p.Type == "olm.package.required" { + var req struct { + PackageName string `json:"packageName"` + } + if err := json.Unmarshal(p.Value, &req); err == nil && req.PackageName == packageName { + dependents = append(dependents, fmt.Sprintf("%s/%s", sub.Namespace, sub.Name)) + break + } + } + } + } + return dependents +} + // Gather collects and returns everything that would be migrated without making // any cluster mutations — backs the CLI convert --dry-run (R1.1). func (m *Migrator) Gather(ctx context.Context, opts Options) (*MigrationInfo, error) { From c2468abb4ecb9ae35bb8dac3cc31d3fc97b6f42c Mon Sep 17 00:00:00 2001 From: Todd Short Date: Mon, 24 Aug 2026 16:07:34 -0400 Subject: [PATCH 22/22] Fix R9: warn about TLS certificate pivot during migration R9 says OLMv0 manages TLS certs directly while OLMv1 delegates to cert-manager (upstream) / openshift-service-ca (downstream). Pod restarts are expected as new cert secrets are provisioned. Adds a progress warning in Migrate() just before CreateClusterObjectSet so the admin is informed this is expected behavior, not a failure. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- migration/pkg/migration/migration.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/migration/pkg/migration/migration.go b/migration/pkg/migration/migration.go index d51bdec..58a9f6c 100644 --- a/migration/pkg/migration/migration.go +++ b/migration/pkg/migration/migration.go @@ -111,6 +111,13 @@ func (m *Migrator) Migrate(ctx context.Context, opts Options) error { } info.CollectedObjects = objects + // R9: warn about TLS certificate pivot. OLMv0 manages certs directly via its own + // cert rotation; OLMv1 delegates to cert-manager (upstream) or openshift-service-ca + // (downstream). Pod restarts are expected during this pivot as the new cert secrets + // are provisioned. This is known behavior and does not indicate a migration failure. + m.progress("Note: TLS certificate management will transfer from OLMv0 to cert-manager/service-ca; " + + "expect pod restarts while new cert secrets are provisioned") + if err := m.CreateClusterObjectSet(ctx, opts, info); err != nil { if recoverErr := m.RecoverBeforeCE(ctx, opts, backup); recoverErr != nil { return fmt.Errorf("COS creation failed: %w; recovery also failed: %v", err, recoverErr)