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/.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/.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/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/go.mod b/go.mod new file mode 100644 index 0000000..0ba77b3 --- /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 + k8s.io/utils v0.0.0-20260626114624-be93311217bd + sigs.k8s.io/controller-runtime v0.24.1 + sigs.k8s.io/yaml v1.6.0 +) + +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 + 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/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/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} 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..035aecb --- /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..aeb098d --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/check.go @@ -0,0 +1,121 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +var ( + 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 { //nolint:nestif + 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 { //nolint:nestif + 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..3f0bef6 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/cleanup.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +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 { //nolint:nestif + 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 { //nolint:nestif + // 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..6e78cb2 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/convert.go @@ -0,0 +1,339 @@ +package main + +import ( + "encoding/json" + "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 { //nolint:nestif + 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 { //nolint:nestif + 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, + BackupDirectory: convertBackupDir, + DeleteOperatorGroup: convertDeleteOG, + AcknowledgeWatchScopeChange: convertAckWatchScope, + AcknowledgeOperatorCondition: convertAckOpCond, + AcknowledgeOLMv0APIAccess: convertAckOLMv0API, + AcknowledgeScopedServiceAccount: convertAckScopedSA, + AcknowledgeNotSteadyState: convertAckNotSteady, + } + 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, + BackupDirectory: convertBackupDir, + DeleteOperatorGroup: convertDeleteOG, + AcknowledgeWatchScopeChange: convertAckWatchScope, + AcknowledgeOperatorCondition: convertAckOpCond, + AcknowledgeOLMv0APIAccess: convertAckOLMv0API, + AcknowledgeScopedServiceAccount: convertAckScopedSA, + AcknowledgeNotSteadyState: convertAckNotSteady, + } + opts.ApplyDefaults() + + if convertDryRun { + return runConvertDryRun(cmd, m, opts) + } + + 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, ip) + if err != nil { + return fmt.Errorf("failed to backup resources: %w", err) + } + // 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)...") + 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) 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..4248e18 --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/output.go @@ -0,0 +1,91 @@ +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 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..1d8312d --- /dev/null +++ b/migration/examples/cmd/migrate-operators-v0-to-v1/rollback.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + + "github.com/operator-framework/library-olm/migration/pkg/migration" +) + +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 { //nolint:nestif + 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 { //nolint:nestif + 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 +} diff --git a/migration/pkg/catalogmigration/catalogmigration.go b/migration/pkg/catalogmigration/catalogmigration.go new file mode 100644 index 0000000..d751cce --- /dev/null +++ b/migration/pkg/catalogmigration/catalogmigration.go @@ -0,0 +1,403 @@ +// 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" +) + +// 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 + Notes []string // informational notices (e.g. dropped fields with no OLMv1 equivalent) +} + +// 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) + + // 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) + 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), + Notes: notes, + }) + 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), + Notes: notes, + }) + 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), + Notes: notes, + }) + 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", + Notes: notes, + }) + + // 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), + Notes: notes, + }) + 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), + Notes: notes, + }) + 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), + Notes: notes, + }) + 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), + Notes: notes, + }) + + // 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 + } + + // 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 0 + } + + interval := cs.Spec.UpdateStrategy.Interval + if interval == nil || interval.Duration == 0 { + return 0 + } + + minutes := int(interval.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..7836dca --- /dev/null +++ b/migration/pkg/migration/catalog.go @@ -0,0 +1,288 @@ +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"` + DefaultChannel string `json:"defaultChannel,omitempty"` + 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 + DefaultChannel string // the package's declared defaultChannel from the FBC + 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 + if meta.DefaultChannel != "" { + info.DefaultChannel = meta.DefaultChannel + } + } + 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..b983951 --- /dev/null +++ b/migration/pkg/migration/collector.go @@ -0,0 +1,459 @@ +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 + 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: ipName, + Namespace: ipNamespace, + }, ip); err != nil { + return nil, nil, nil, fmt.Errorf("failed to get InstallPlan %s: %w", ipName, 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) + + // 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 { + 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 +} + +// 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", + gvk.Group, + gvk.Kind, + obj.GetNamespace(), + obj.GetName()) +} + +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", + }) + + // 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.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..8549301 --- /dev/null +++ b/migration/pkg/migration/compatibility.go @@ -0,0 +1,347 @@ +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)...) + + // 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) + if err != nil { + return nil, err + } + report.Checks = append(report.Checks, condCheck) + + // OLMv0-API RBAC check (C5 — soft) + report.Checks = append(report.Checks, checkOLMv0APIAccess(opts, csv)) + + 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 — soft) + if og.Spec.ServiceAccountName != "" { + 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", + Passed: true, + Message: "OperatorGroup does not use a scoped service account", + }) + } + + // spec.selector — scoped namespace selector means not AllNamespaces (C1 — soft) + if og.Spec.Selector != nil && !isEmptyLabelSelector(og.Spec.Selector) { + 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", + Passed: true, + Message: "OperatorGroup does not use a namespace selector", + }) + } + + // 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: 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{ + Name: "Upgrade strategy", + Passed: true, + Message: "upgrade strategy is Default or unset", + }) + } + + // spec.targetNamespaces — AllNamespaces mode (C1 — soft) + if len(og.Spec.TargetNamespaces) > 0 { + 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", + Passed: true, + Message: "operator is in AllNamespaces mode", + }) + } + + // status.namespaces warning — single-namespace targets will become AllNamespaces (C1 — soft) + if len(og.Status.Namespaces) == 1 && 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 +} + +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 +} + +// 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) { + 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 { + 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, + 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..c95aaf3 --- /dev/null +++ b/migration/pkg/migration/labels.go @@ -0,0 +1,46 @@ +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" //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. + 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" + + // 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..58a9f6c --- /dev/null +++ b/migration/pkg/migration/migration.go @@ -0,0 +1,836 @@ +package migration + +import ( + "context" + "encoding/json" + "fmt" + "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" + "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, 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) + } + 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 + + // 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) + } + 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, 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, + Namespace: opts.SubscriptionNamespace, + }, &sub); err != nil { + 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 +} + +// 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. +// +// 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) + systemNS := opts.systemNamespace() + + 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) + + // 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). + 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 (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: annotations, + }, + 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 + } + + // 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 != "" { + ce.Spec.Source.Catalog.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + LabelMetadataName: info.ResolvedCatalogName, + }, + } + } + + // 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) + } + + 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 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{ + 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..9c696d9 --- /dev/null +++ b/migration/pkg/migration/readiness.go @@ -0,0 +1,156 @@ +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 (C8 — soft; same flag as CSV health) + 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 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; pass --acknowledge-not-steady-state to override", 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 != "" { //nolint:nestif + 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 { + 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 { + 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", + 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..8a51d89 --- /dev/null +++ b/migration/pkg/migration/scan.go @@ -0,0 +1,538 @@ +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 + Reason string // human-readable explanation of the status (R1.3) + Eligible bool // true when Status == Eligible (backwards compat) + // 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 +// 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.Reason = "both Subscription and annotated ClusterExtension exist; resolve with cleanup or rollback" + result.Eligible = false + result.Error = fmt.Errorf("%s", result.Reason) + 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.Reason = err.Error() + 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.Reason = fmt.Sprintf("failed to get CSV: %v", err) + 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.Reason = fmt.Sprintf("compatibility check error: %v", err) + result.Error = fmt.Errorf("compatibility check error: %w", err) + results = append(results, result) + continue + } + + // Merge readiness + compat failed checks + result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) + + // 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 { //nolint:nestif + 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.Reason = fmt.Sprintf("package %q not found in any serving ClusterCatalog", sub.Spec.Package) + 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.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 + result.Reason = fmt.Sprintf("%d check(s) failed", len(result.FailedChecks)) + 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, + 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), + }) + } + + 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.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 + } + } + + readiness, err := m.CheckReadiness(ctx, opts) + if err != nil { + return nil, err + } + + 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 = sub.Spec.Package + 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.Reason = err.Error() + result.Error = err + return result, nil + } + + result.FailedChecks = append(readiness.FailedChecks(), compat.FailedChecks()...) + 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 + // 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 +} + +// 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 +} + +// 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) +} + +// ── 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) +} + +// 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) { + 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/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 new file mode 100644 index 0000000..b80f55f --- /dev/null +++ b/migration/pkg/migration/types.go @@ -0,0 +1,163 @@ +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" +) + +// 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 + + // 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 + + // 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. +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 + + // 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). + OperatorGroupBackupJSON string +} + +// 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 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) +}