diff --git a/.bingo/OWNERS b/.bingo/OWNERS new file mode 100644 index 0000000000..835cabe507 --- /dev/null +++ b/.bingo/OWNERS @@ -0,0 +1,2 @@ +approvers: + - ci-approvers diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 16a0e58a23..fe814c5176 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -23,11 +23,11 @@ $(BINGO): $(BINGO_DIR)/bingo.mod @echo "(re)installing $(GOBIN)/bingo-v0.9.0" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=bingo.mod -o=$(GOBIN)/bingo-v0.9.0 "github.com/bwplotka/bingo" -CONTROLLER_GEN := $(GOBIN)/controller-gen-v0.17.3 +CONTROLLER_GEN := $(GOBIN)/controller-gen-v0.19.0 $(CONTROLLER_GEN): $(BINGO_DIR)/controller-gen.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)/controller-gen-v0.17.3" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=controller-gen.mod -o=$(GOBIN)/controller-gen-v0.17.3 "sigs.k8s.io/controller-tools/cmd/controller-gen" + @echo "(re)installing $(GOBIN)/controller-gen-v0.19.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=controller-gen.mod -o=$(GOBIN)/controller-gen-v0.19.0 "sigs.k8s.io/controller-tools/cmd/controller-gen" CRD_DIFF := $(GOBIN)/crd-diff-v0.2.0 $(CRD_DIFF): $(BINGO_DIR)/crd-diff.mod @@ -41,11 +41,17 @@ $(CRD_REF_DOCS): $(BINGO_DIR)/crd-ref-docs.mod @echo "(re)installing $(GOBIN)/crd-ref-docs-v0.1.0" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-ref-docs.mod -o=$(GOBIN)/crd-ref-docs-v0.1.0 "github.com/elastic/crd-ref-docs" -GOLANGCI_LINT := $(GOBIN)/golangci-lint-v1.64.6 +GOJQ := $(GOBIN)/gojq-v0.12.17 +$(GOJQ): $(BINGO_DIR)/gojq.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)/gojq-v0.12.17" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=gojq.mod -o=$(GOBIN)/gojq-v0.12.17 "github.com/itchyny/gojq/cmd/gojq" + +GOLANGCI_LINT := $(GOBIN)/golangci-lint-v2.1.6 $(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-v1.64.6" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v1.64.6 "github.com/golangci/golangci-lint/cmd/golangci-lint" + @echo "(re)installing $(GOBIN)/golangci-lint-v2.1.6" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v2.1.6 "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" GORELEASER := $(GOBIN)/goreleaser-v1.26.2 $(GORELEASER): $(BINGO_DIR)/goreleaser.mod @@ -53,11 +59,17 @@ $(GORELEASER): $(BINGO_DIR)/goreleaser.mod @echo "(re)installing $(GOBIN)/goreleaser-v1.26.2" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=goreleaser.mod -o=$(GOBIN)/goreleaser-v1.26.2 "github.com/goreleaser/goreleaser" -KIND := $(GOBIN)/kind-v0.27.0 +HELM := $(GOBIN)/helm-v3.18.4 +$(HELM): $(BINGO_DIR)/helm.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)/helm-v3.18.4" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=helm.mod -o=$(GOBIN)/helm-v3.18.4 "helm.sh/helm/v3/cmd/helm" + +KIND := $(GOBIN)/kind-v0.30.0 $(KIND): $(BINGO_DIR)/kind.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/kind-v0.27.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.27.0 "sigs.k8s.io/kind" + @echo "(re)installing $(GOBIN)/kind-v0.30.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.30.0 "sigs.k8s.io/kind" KUSTOMIZE := $(GOBIN)/kustomize-v5.6.0 $(KUSTOMIZE): $(BINGO_DIR)/kustomize.mod @@ -77,9 +89,15 @@ $(OPM): $(BINGO_DIR)/opm.mod @echo "(re)installing $(GOBIN)/opm-v1.51.0" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=opm.mod -o=$(GOBIN)/opm-v1.51.0 "github.com/operator-framework/operator-registry/cmd/opm" -SETUP_ENVTEST := $(GOBIN)/setup-envtest-v0.0.0-20250304084143-6eb011f4f89e +SETUP_ENVTEST := $(GOBIN)/setup-envtest-v0.0.0-20250620151452-b9a9ca01fd37 $(SETUP_ENVTEST): $(BINGO_DIR)/setup-envtest.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)/setup-envtest-v0.0.0-20250304084143-6eb011f4f89e" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=setup-envtest.mod -o=$(GOBIN)/setup-envtest-v0.0.0-20250304084143-6eb011f4f89e "sigs.k8s.io/controller-runtime/tools/setup-envtest" + @echo "(re)installing $(GOBIN)/setup-envtest-v0.0.0-20250620151452-b9a9ca01fd37" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=setup-envtest.mod -o=$(GOBIN)/setup-envtest-v0.0.0-20250620151452-b9a9ca01fd37 "sigs.k8s.io/controller-runtime/tools/setup-envtest" + +YAMLFMT := $(GOBIN)/yamlfmt-v0.20.0 +$(YAMLFMT): $(BINGO_DIR)/yamlfmt.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)/yamlfmt-v0.20.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=yamlfmt.mod -o=$(GOBIN)/yamlfmt-v0.20.0 "github.com/google/yamlfmt/cmd/yamlfmt" diff --git a/.bingo/controller-gen.mod b/.bingo/controller-gen.mod index 7c14f3cca8..3c92fc0a00 100644 --- a/.bingo/controller-gen.mod +++ b/.bingo/controller-gen.mod @@ -1,7 +1,7 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.23.0 +go 1.24.0 -toolchain go1.23.4 +toolchain go1.24.3 -require sigs.k8s.io/controller-tools v0.17.3 // cmd/controller-gen +require sigs.k8s.io/controller-tools v0.19.0 // cmd/controller-gen diff --git a/.bingo/controller-gen.sum b/.bingo/controller-gen.sum index 7ed7f91246..3bb68478c8 100644 --- a/.bingo/controller-gen.sum +++ b/.bingo/controller-gen.sum @@ -3,6 +3,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= @@ -17,6 +18,8 @@ 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= @@ -27,6 +30,16 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/gobuffalo/flect v0.2.5 h1:H6vvsv2an0lalEaCDRThvtBfmg44W/QHXBCYUXf/6S4= github.com/gobuffalo/flect v0.2.5/go.mod h1:1ZyCLIbg0YD7sDkzvFdPoOydPtD8y9JQnrOROolUcM8= github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= @@ -35,6 +48,10 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= @@ -45,10 +62,18 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -65,6 +90,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/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/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -82,6 +109,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -94,6 +123,10 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= @@ -113,6 +146,10 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +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/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -131,6 +168,10 @@ golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -142,6 +183,10 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -163,6 +208,10 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= @@ -177,6 +226,10 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -195,11 +248,20 @@ golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -224,6 +286,10 @@ k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/apiextensions-apiserver v0.25.0 h1:CJ9zlyXAbq0FIW8CD7HHyozCMBpDSiH7EdrSTCZcZFY= k8s.io/apiextensions-apiserver v0.25.0/go.mod h1:3pAjZiN4zw7R8aZC5gR0y3/vCkGlAjCazcg1me8iB/E= k8s.io/apiextensions-apiserver v0.27.1 h1:Hp7B3KxKHBZ/FxmVFVpaDiXI6CCSr49P1OJjxKO6o4g= @@ -240,6 +306,10 @@ k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+ k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= k8s.io/apimachinery v0.25.0 h1:MlP0r6+3XbkUG2itd6vp3oxbtdQLQI94fD5gCS+gnoU= k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0= k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc= @@ -256,6 +326,18 @@ k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4= +k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o= +k8s.io/code-generator v0.34.0 h1:Ze2i1QsvUprIlX3oHiGv09BFQRLCz+StA8qKwwFzees= +k8s.io/code-generator v0.34.0/go.mod h1:Py2+4w2HXItL8CGhks8uI/wS3Y93wPKO/9mBQUYNua0= +k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= +k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= @@ -267,6 +349,10 @@ k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= @@ -277,6 +363,8 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-tools v0.10.0 h1:0L5DTDTFB67jm9DkfrONgTGmfc/zYow0ZaHyppizU2U= sigs.k8s.io/controller-tools v0.10.0/go.mod h1:uvr0EW6IsprfB0jpQq6evtKy+hHyHCXNfdWI5ONPx94= sigs.k8s.io/controller-tools v0.12.0 h1:TY6CGE6+6hzO7hhJFte65ud3cFmmZW947jajXkuDfBw= @@ -293,19 +381,34 @@ sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPF sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0= sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw= sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= +sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9xrsE= +sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U= +sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= +sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +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/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +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/.bingo/gojq.mod b/.bingo/gojq.mod new file mode 100644 index 0000000000..004aae3b13 --- /dev/null +++ b/.bingo/gojq.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.24.4 + +require github.com/itchyny/gojq v0.12.17 // cmd/gojq diff --git a/.bingo/gojq.sum b/.bingo/gojq.sum new file mode 100644 index 0000000000..e87b5b0e34 --- /dev/null +++ b/.bingo/gojq.sum @@ -0,0 +1,17 @@ +github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= +github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= +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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.bingo/golangci-lint.mod b/.bingo/golangci-lint.mod index fe81a6a054..07ecc9aa88 100644 --- a/.bingo/golangci-lint.mod +++ b/.bingo/golangci-lint.mod @@ -1,5 +1,7 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.23.0 +go 1.24.2 -require github.com/golangci/golangci-lint v1.64.6 // cmd/golangci-lint +toolchain go1.24.3 + +require github.com/golangci/golangci-lint/v2 v2.1.6 // cmd/golangci-lint diff --git a/.bingo/golangci-lint.sum b/.bingo/golangci-lint.sum index 1c749d80d2..17881e374b 100644 --- a/.bingo/golangci-lint.sum +++ b/.bingo/golangci-lint.sum @@ -1,9 +1,5 @@ -4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= -4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= 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.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= -4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= 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= @@ -11,7 +7,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT 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.44.3/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= @@ -22,9 +17,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV 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 v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 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= @@ -42,92 +34,31 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo 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= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/4meepo/tagalign v1.2.2 h1:kQeUTkFTaBRtd/7jm8OKJl9iHk0gAO+TDFPHGSna0aw= -github.com/4meepo/tagalign v1.2.2/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= -github.com/4meepo/tagalign v1.3.3 h1:ZsOxcwGD/jP4U/aw7qeWu58i7dwYemfy5Y+IF1ACoNw= -github.com/4meepo/tagalign v1.3.3/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= -github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= -github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= -github.com/4meepo/tagalign v1.4.1 h1:GYTu2FaPGOGb/xJalcqHeD4il5BiCywyEYZOA55P6J4= -github.com/4meepo/tagalign v1.4.1/go.mod h1:2H9Yu6sZ67hmuraFgfZkNcg5Py9Ch/Om9l2K/2W1qS4= github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= -github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU= -github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA= -github.com/Abirdcfly/dupword v0.0.14 h1:3U4ulkc8EUo+CaT105/GJ1BQwtgyj6+VaBVbAX11Ba8= -github.com/Abirdcfly/dupword v0.0.14/go.mod h1:VKDAbxdY8YbKUByLGg8EETzYSuC4crm9WwI6Y3S0cLI= -github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY= -github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM= github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= -github.com/Antonboom/errname v0.1.10 h1:RZ7cYo/GuZqjr1nuJLNe8ZH+a+Jd9DaZzttWzak9Bls= -github.com/Antonboom/errname v0.1.10/go.mod h1:xLeiCIrvVNpUtsN0wxAh05bNIZpqE22/qDMnTBTttiA= -github.com/Antonboom/errname v0.1.12 h1:oh9ak2zUtsLp5oaEd/erjB4GPu9w19NyoIskZClDcQY= -github.com/Antonboom/errname v0.1.12/go.mod h1:bK7todrzvlaZoQagP1orKzWXv59X/x0W0Io2XT1Ssro= -github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= -github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= -github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= -github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= -github.com/Antonboom/nilnil v0.1.5 h1:X2JAdEVcbPaOom2TUa1FxZ3uyuUlex0XMLGYMemu6l0= -github.com/Antonboom/nilnil v0.1.5/go.mod h1:I24toVuBKhfP5teihGWctrRiPbRKHwZIFOvc6v3HZXk= -github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTow= -github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= -github.com/Antonboom/nilnil v0.1.8 h1:97QG7xrLq4TBK2U9aFq/I8Mcgz67pwMIiswnTA9gIn0= -github.com/Antonboom/nilnil v0.1.8/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= -github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= -github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= -github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= -github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= -github.com/Antonboom/testifylint v1.2.0 h1:015bxD8zc5iY8QwTp4+RG9I4kIbqwvGX9TrBbb7jGdM= -github.com/Antonboom/testifylint v1.2.0/go.mod h1:rkmEqjqVnHDRNsinyN6fPSLnoajzFwsCcguJgwADBkw= -github.com/Antonboom/testifylint v1.3.1 h1:Uam4q1Q+2b6H7gvk9RQFw6jyVDdpzIirFOOrbs14eG4= -github.com/Antonboom/testifylint v1.3.1/go.mod h1:NV0hTlteCkViPW9mSR4wEMfwp+Hs1T3dY60bkvSfhpM= -github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck= -github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA= -github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= -github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= +github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE= +github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw= +github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng= +github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE= +github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o= +github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= -github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.2.2 h1:OrFlsDdOj9hW/oBEJBNSuH7QWf+E9WPVHw+x52bXVbk= -github.com/Crocmagnon/fatcontext v0.2.2/go.mod h1:WSn/c/+MMNiD8Pri0ahRj0o9jVpeowzavOQplBJw6u0= -github.com/Crocmagnon/fatcontext v0.4.0 h1:4ykozu23YHA0JB6+thiuEv7iT6xq995qS1vcuWZq0tg= -github.com/Crocmagnon/fatcontext v0.4.0/go.mod h1:ZtWrXkgyfsYPzS6K3O88va6t2GEglG93vnII/F94WC0= -github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= -github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= -github.com/Crocmagnon/fatcontext v0.5.3 h1:zCh/wjc9oyeF+Gmp+V60wetm8ph2tlsxocgg/J0hOps= -github.com/Crocmagnon/fatcontext v0.5.3/go.mod h1:XoCQYY1J+XTfyv74qLXvNw4xFunr3L1wkopIIKG7wGM= -github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= -github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 h1:+r1rSv4gvYn0wmRjC8X7IAzX8QezqtFV9m0MUHFJgts= -github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0/go.mod h1:b3g59n2Y+T5xmcxJL+UEG2f8cQploZm1mR/v6BW0mU0= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0/go.mod h1:Nl76DrGNJTA1KJ0LePKBw/vznBX1EHbAZX8mwjR82nI= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= -github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= -github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= -github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= -github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= -github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= +github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 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= @@ -135,177 +66,102 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy 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.2 h1:qnXuZNvv3/AxkAb22q/sEsEpcA99YxLFACDtEw9TPxE= -github.com/alexkohler/nakedret/v2 v2.0.2/go.mod h1:2b8Gkk0GsOrqQv/gPWjNLDSKwG8I5moSXG1K4VIBcTQ= -github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= -github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= -github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= -github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +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.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= 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.1.1 h1:7cYuJewpy9jFNMEA72Q1+3Nm3zKHzg+Q28D5f2bBFUA= -github.com/alingse/nilnesserr v0.1.1/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= -github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/ashanbrown/forbidigo v1.5.3 h1:jfg+fkm/snMx+V9FBwsl1d340BV/99kZGv5jN9hBoXk= -github.com/ashanbrown/forbidigo v1.5.3/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +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 v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= -github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +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.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= -github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= 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/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= -github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= -github.com/bombsimon/wsl/v4 v4.2.1 h1:Cxg6u+XDWff75SIFFmNsqnIOgob+Q9hG6y/ioKbRFiM= -github.com/bombsimon/wsl/v4 v4.2.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= -github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= -github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A= -github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc= -github.com/breml/bidichk v0.2.4 h1:i3yedFWWQ7YzjdZJHnPo9d/xURinSq3OM+gyM43K4/8= -github.com/breml/bidichk v0.2.4/go.mod h1:7Zk0kRFt1LIZxtQdl9W9JwGAcLTTkOs+tN7wuEYGJ3s= -github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= -github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= -github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= -github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= -github.com/breml/errchkjson v0.3.1 h1:hlIeXuspTyt8Y/UmP5qy1JocGNR00KQHgfaNtRAjoxQ= -github.com/breml/errchkjson v0.3.1/go.mod h1:XroxrzKjdiutFyW3nWhw34VGg7kiMsDQox73yWCGI2U= -github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= -github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= -github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= -github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= -github.com/butuzov/ireturn v0.2.0 h1:kCHi+YzC150GE98WFuZQu9yrTn6GEydO2AuPLbTgnO4= -github.com/butuzov/ireturn v0.2.0/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= -github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= -github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= -github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= -github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= -github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= -github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= -github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= -github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= +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/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.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= -github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= -github.com/catenacyber/perfsprint v0.8.1 h1:bGOHuzHe0IkoGeY831RW4aSlt1lPRd3WRAScSWOaV7E= -github.com/catenacyber/perfsprint v0.8.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= -github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw= -github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= +github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD2z0vls0= +github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= 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 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 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.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 h1:W9o46d2kbNL06lq7UNDPV0zYLzkrde/bjIqO02eoll0= -github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8/go.mod h1:gakxgyXaaPkxvLw1XQxNGK4I37ys9iBRzNUx/B7pUCo= +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/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= 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.1.1 h1:gHe4LfqCspWkh8KpJFs20fJz3XRHFBFUV9yI7Itu83Q= -github.com/ckaznocha/intrange v0.1.1/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= -github.com/ckaznocha/intrange v0.1.2 h1:3Y4JAxcMntgb/wABQ6e8Q8leMd26JbX2790lIss9MTI= -github.com/ckaznocha/intrange v0.1.2/go.mod h1:RWffCw/vKBwHeOEwWdCikAtY0q4gGt8VhJZEEA5n+RE= -github.com/ckaznocha/intrange v0.2.0 h1:FykcZuJ8BD7oX93YbO1UY9oZtkRbp+1/kJcDjkefYLs= -github.com/ckaznocha/intrange v0.2.0/go.mod h1:r5I7nUlAAG56xmkOpw4XVr16BXhwYTUdcuRFeevn1oE= -github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= -github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= +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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= -github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= 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.10.1 h1:eheNA3ljF6SxnPD/vE4lCBusVHmV3Rs3dkKvFrJ7MR0= -github.com/daixiang0/gci v0.10.1/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= -github.com/daixiang0/gci v0.12.3 h1:yOZI7VAxAGPQmkb1eqt5g/11SUlwoat1fSblGLmdiQc= -github.com/daixiang0/gci v0.12.3/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= -github.com/daixiang0/gci v0.13.4 h1:61UGkmpoAcxHM2hhNkZEf5SzwQtWJXTSws7jaPyqwlw= -github.com/daixiang0/gci v0.13.4/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= -github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= -github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8= +github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +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.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= -github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= 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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= -github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= -github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= -github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= 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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 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.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= -github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= -github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= -github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= +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.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbxOK4Ug= -github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= -github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= -github.com/ghostiam/protogetter v0.3.8 h1:LYcXbYvybUyTIxN2Mj9h6rHrDZBDwZloPoKctWrFyJY= -github.com/ghostiam/protogetter v0.3.8/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= -github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= -github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= -github.com/go-critic/go-critic v0.8.1 h1:16omCF1gN3gTzt4j4J6fKI/HnRojhEp+Eks6EuKw3vw= -github.com/go-critic/go-critic v0.8.1/go.mod h1:kpzXl09SIJX1cr9TB/g/sAG+eFEl7ZS9f9cqvZtyNl0= -github.com/go-critic/go-critic v0.11.2 h1:81xH/2muBphEgPtcwH1p6QD+KzXl2tMSi3hXjBSxDnM= -github.com/go-critic/go-critic v0.11.2/go.mod h1:OePaicfjsf+KPy33yq4gzv6CO7TEQ9Rom6ns1KsJnl8= -github.com/go-critic/go-critic v0.11.3 h1:SJbYD/egY1noYjTMNTlhGaYlfQ77rQmrNH7h+gtn0N0= -github.com/go-critic/go-critic v0.11.3/go.mod h1:Je0h5Obm1rR5hAGA9mP2PDiOOk53W+n7pyvXErFKIgI= -github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= -github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= -github.com/go-critic/go-critic v0.11.5 h1:TkDTOn5v7EEngMxu8KbuFqFR43USaaH8XRJLz1jhVYA= -github.com/go-critic/go-critic v0.11.5/go.mod h1:wu6U7ny9PiaHaZHcvMDmdysMqvDem162Rh3zWTrqk8M= -github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= -github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= +github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY= +github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY= +github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI= 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= @@ -321,7 +177,6 @@ github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4 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 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= 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= @@ -334,22 +189,12 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi 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.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= -github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -379,74 +224,27 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD 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 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6J5HIP8ZtyMdiDscjMLfRBSPuzVVeo= -github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= +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.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= -github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 h1:amWTbTGqOZ71ruzrdA+Nx5WA3tV1N0goTspwmKCQvBY= -github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2/go.mod h1:9wOXstvyDRshQ9LggQuzBCGysxs3b6Uo/1MvYCR2NMs= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= -github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= -github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= -github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9 h1:t5wybL6RtO83VwoMOb7U/Peqe3gGKQlPIC66wXmnkvM= -github.com/golangci/gofmt v0.0.0-20241223200906-057b0627d9b9/go.mod h1:Ag3L7sh7E28qAp/5xnpMMTuGYqxLZoSaEHZDkZB1RgU= 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 v1.53.3 h1:CUcRafczT4t1F+mvdkUm6KuOpxUZTl0yWN/rSU6sSMo= -github.com/golangci/golangci-lint v1.53.3/go.mod h1:W4Gg3ONq6p3Jl+0s/h9Gr0j7yEgHJWWZO2bHl2tBUXM= -github.com/golangci/golangci-lint v1.57.2 h1:NNhxfZyL5He1WWDrIvl1a4n5bvWZBcgAqBwlJAAgLTw= -github.com/golangci/golangci-lint v1.57.2/go.mod h1:ApiG3S3Ca23QyfGp5BmsorTiVxJpr5jGiNS0BkdSidg= -github.com/golangci/golangci-lint v1.58.0 h1:r8duFARMJ0VdSM9tDXAdt2+f57dfZQmagvYX6kmkUKQ= -github.com/golangci/golangci-lint v1.58.0/go.mod h1:WAY3BnSLvTUEv41Q0v3ZFzNybLRF+a7Vd9Da8Jx9Eqo= -github.com/golangci/golangci-lint v1.59.1 h1:CRRLu1JbhK5avLABFJ/OHVSQ0Ie5c4ulsOId1h3TTks= -github.com/golangci/golangci-lint v1.59.1/go.mod h1:jX5Oif4C7P0j9++YB2MMJmoNrb01NJ8ITqKWNLewThg= -github.com/golangci/golangci-lint v1.60.3 h1:l38A5de24ZeDlcFF+EB7m3W5joPD99/hS5SIHJPyZa0= -github.com/golangci/golangci-lint v1.60.3/go.mod h1:J4vOpcjzRI+lDL2DKNGBZVB3EQSBfCBCMpaydWLtJNo= -github.com/golangci/golangci-lint v1.61.0 h1:VvbOLaRVWmyxCnUIMTbf1kDsaJbTzH20FAMXTAlQGu8= -github.com/golangci/golangci-lint v1.61.0/go.mod h1:e4lztIrJJgLPhWvFPDkhiMwEFRrWlmFbrZea3FsJyN8= -github.com/golangci/golangci-lint v1.63.4 h1:bJQFQ3hSfUto597dkL7ipDzOxsGEpiWdLiZ359OWOBI= -github.com/golangci/golangci-lint v1.63.4/go.mod h1:Hx0B7Lg5/NXbaOHem8+KU+ZUIzMI6zNj/7tFwdnn10I= -github.com/golangci/golangci-lint v1.64.5 h1:5omC86XFBKXZgCrVdUWU+WNHKd+CWCxNx717KXnzKZY= -github.com/golangci/golangci-lint v1.64.5/go.mod h1:WZnwq8TF0z61h3jLQ7Sk5trcP7b3kUFxLD6l1ivtdvU= -github.com/golangci/golangci-lint v1.64.6 h1:jOLaQN41IV7bMzXuNC4UnQGll7N1xY6eFDXkXEPGKAs= -github.com/golangci/golangci-lint v1.64.6/go.mod h1:Wz9q+6EVuqGQ94GQ96RB2mjpcZYTOGhBhbt4O7REPu4= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.4.0 h1:KtVB/hTK4bbL/S6bs64rYyk8adjmh1BygbBiaAiX+a0= -github.com/golangci/misspell v0.4.0/go.mod h1:W6O/bwV6lGDxUCChm2ykw9NQdd5bYd1Xkjo88UcWyJc= -github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= -github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= -github.com/golangci/misspell v0.5.1 h1:/SjR1clj5uDjNLwYzCahHwIOPmQgoH04AyQIiWGbhCM= -github.com/golangci/misspell v0.5.1/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w= +github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8= +github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8= +github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= -github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= -github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 h1:DIPQnGy2Gv2FSA4B/hh8Q7xx3B7AIDk3DAMeHclH1vQ= -github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6/go.mod h1:0AKcRCkMoKvUvlf89F6O7H2LYdhr1zBh736mBItOdRs= -github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= -github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= -github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs= -github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +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= @@ -461,16 +259,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/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/martian/v3 v3.1.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= @@ -478,41 +271,25 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf 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/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= -github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= 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.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= -github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= 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.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= -github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= 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.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 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-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -524,23 +301,12 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T 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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/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.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= -github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= -github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= -github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jgautheron/goconst v1.8.1 h1:PPqCYp3K/xlOj5JmIe6O1Mj6r1DbkdbLtR3AJuZo414= +github.com/jgautheron/goconst v1.8.1/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/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= -github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= -github.com/jjti/go-spancheck v0.5.3 h1:vfq4s2IB8T3HvbpiwDTYgVPj1Ze/ZSXrTtaZRTc7CuM= -github.com/jjti/go-spancheck v0.5.3/go.mod h1:eQdOX1k3T+nAKvZDyLC3Eby0La4dZ+I19iOl5NzSPFE= -github.com/jjti/go-spancheck v0.6.1 h1:ZK/wE5Kyi1VX3PJpUO2oEgeoI4FWOUm7Shb2Gbv5obI= -github.com/jjti/go-spancheck v0.6.1/go.mod h1:vF1QkOO159prdo6mHRxak2CpzDpHAfKiPUDP/NeRnX8= -github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= -github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -552,125 +318,65 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 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.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= -github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= 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.0.10 h1:8HYDy6KQYqTmD7JuhZMWS1nwPru9889XI24ROd/+WXI= -github.com/karamaru-alpha/copyloopvar v1.0.10/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= -github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= -github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= -github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= -github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= -github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= -github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= -github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= -github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= 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 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= -github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= -github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= -github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= 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/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.7 h1:2uCk94js0+nVNQoHZNLBkAR1DQJrVzw6T0RMzJn55dQ= -github.com/kunwardeep/paralleltest v1.0.7/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= -github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= -github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= -github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= -github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= -github.com/lasiar/canonicalheader v1.0.6 h1:LJiiZ/MzkqibXOL2v+J8+WZM21pM0ivrBY/jbm9f5fo= -github.com/lasiar/canonicalheader v1.0.6/go.mod h1:GfXTLQb3O1qF5qcSTyXTnfNUggUNyzbkOSpzZ0dpUJo= -github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= -github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= +github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8= +github.com/kunwardeep/paralleltest v1.0.14/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.3.1 h1:90yWWoAKMFHeovTK8uzBms9Ppp8Du/xQ20DRO26Ymrw= -github.com/ldez/exptostd v0.3.1/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/exptostd v0.4.1 h1:DIollgQ3LWZMp3HJbSXsdE2giJxMfjyHj3eX4oiD6JU= -github.com/ldez/exptostd v0.4.1/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs= -github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= -github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= -github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= -github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= -github.com/ldez/gomoddirectives v0.6.0 h1:Jyf1ZdTeiIB4dd+2n4qw+g4aI9IJ6JyfOZ8BityWvnA= -github.com/ldez/gomoddirectives v0.6.0/go.mod h1:TuwOGYoPAoENDWQpe8DMqEm5nIfjrxZXmxX/CExWyZ4= +github.com/ldez/exptostd v0.4.3 h1:Ag1aGiq2epGePuRJhez2mzOpZ8sI9Gimcb4Sb3+pk9Y= +github.com/ldez/exptostd v0.4.3/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= -github.com/ldez/grignotin v0.7.0 h1:vh0dI32WhHaq6LLPZ38g7WxXuZ1+RzyrJ7iPG9JMa8c= -github.com/ldez/grignotin v0.7.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= -github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= -github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= -github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= -github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= -github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= -github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/ldez/usetesting v0.4.3 h1:pJpN0x3fMupdTf/IapYjnkhiY1nSTN+pox1/GyBRw3k= +github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= -github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= -github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= -github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= +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/funcorder v0.2.1 h1:7QJsw3qhljoZ5rH0xapIvjw31EcQeFbF31/7kQ/xS34= +github.com/manuelarte/funcorder v0.2.1/go.mod h1:BQQ0yW57+PF9ZpjpeJDKOffEsQbxDFKW8F8zSMe/Zd0= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= -github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= 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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 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/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= -github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= -github.com/mgechev/revive v1.3.2 h1:Wb8NQKBaALBJ3xrrj4zpwJwqwNA6nDpyJSEQWcCka6U= -github.com/mgechev/revive v1.3.2/go.mod h1:UCLtc7o5vg5aXCwdUTU1kEBQ1v+YXPAkYDIDXbrs5I0= -github.com/mgechev/revive v1.3.7 h1:502QY0vQGe9KtYJ9FpxMz9rL+Fc/P13CI5POL4uHCcE= -github.com/mgechev/revive v1.3.7/go.mod h1:RJ16jUbF0OWC3co/+XTxmFNgEpUPwnnA0BRllX2aDNA= -github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= -github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= -github.com/mgechev/revive v1.5.1 h1:hE+QPeq0/wIzJwOphdVyUJ82njdd8Khp4fUIHGZHW3M= -github.com/mgechev/revive v1.5.1/go.mod h1:lC9AhkJIBs5zwx8wkudyHrU+IJkrEKmpCmGMnIJPk4o= -github.com/mgechev/revive v1.6.1 h1:ncK0ZCMWtb8GXwVAmk+IeWF2ULIDsvRxSRfg5sTwQ2w= -github.com/mgechev/revive v1.6.1/go.mod h1:/2tfHWVO8UQi/hqJsIYNEKELi+DJy/e+PQpLgTB1v88= -github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= -github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= +github.com/mgechev/revive v1.9.0 h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM= +github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo= 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= @@ -680,30 +386,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= -github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= 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/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= -github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= -github.com/nishanths/exhaustive v0.11.0 h1:T3I8nUGhl/Cwu5Z2hfc92l0e04D2GEW6e0l8pzda2l0= -github.com/nishanths/exhaustive v0.11.0/go.mod h1:RqwDsZ1xY0dNdqHho2z6X+bgzizwbLYOWnZbbl2wLB4= 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.12.1 h1:vwOqb5Nu05OikTXqhvLdHCGcx5uthIYIl0t79UVrERQ= -github.com/nunnatsa/ginkgolinter v0.12.1/go.mod h1:AK8Ab1PypVrcGUusuKD8RDcl2KgsIwvNaaxAlyHSzso= -github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= -github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= -github.com/nunnatsa/ginkgolinter v0.18.4 h1:zmX4KUR+6fk/vhUFt8DOP6KwznekhkmVSzzVJve2vyM= -github.com/nunnatsa/ginkgolinter v0.18.4/go.mod h1:AMEane4QQ6JwFz5GgjI5xLUM9S/CylO+UyM97fN2iBI= -github.com/nunnatsa/ginkgolinter v0.19.0 h1:CnHRFAeBS3LdLI9h+Jidbcc5KH71GKOmaBZQk8Srnto= -github.com/nunnatsa/ginkgolinter v0.19.0/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -715,34 +409,15 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9 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.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/polyfloyd/go-errorlint v1.4.2 h1:CU+O4181IxFDdPH6t/HT7IiDj1I7zxNi1RIUxYwn8d0= -github.com/polyfloyd/go-errorlint v1.4.2/go.mod h1:k6fU/+fQe38ednoZS51T7gSIGQW1y94d6TkSr35OzH8= -github.com/polyfloyd/go-errorlint v1.4.8 h1:jiEjKDH33ouFktyez7sckv6pHWif9B7SuS8cutDXFHw= -github.com/polyfloyd/go-errorlint v1.4.8/go.mod h1:NNCxFcFjZcw3xNjVdCchERkEM6Oz7wta2XJVxRftwO4= -github.com/polyfloyd/go-errorlint v1.5.1 h1:5gHxDjLyyWij7fhfrjYNNlHsUNQeyx0LFQKUelO3RBo= -github.com/polyfloyd/go-errorlint v1.5.1/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= -github.com/polyfloyd/go-errorlint v1.5.2 h1:SJhVik3Umsjh7mte1vE0fVZ5T1gznasQG3PV7U5xFdA= -github.com/polyfloyd/go-errorlint v1.5.2/go.mod h1:sH1QC1pxxi0fFecsVIzBmxtrgd9IF/SkJpA6wqyKAJs= -github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= -github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= -github.com/polyfloyd/go-errorlint v1.7.0 h1:Zp6lzCK4hpBDj8y8a237YK4EPrMXQWvOe3nGoH4pFrU= -github.com/polyfloyd/go-errorlint v1.7.0/go.mod h1:dGWKu85mGHnegQ2SWpEybFityCg3j7ZbwsVUxAOk9gY= -github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA= -github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= +github.com/polyfloyd/go-errorlint v1.8.0 h1:DL4RestQqRLr8U4LygLw8g2DX6RN1eBJOpa2mzsrl1Q= +github.com/polyfloyd/go-errorlint v1.8.0/go.mod h1:G2W0Q5roxbLCt0ZQbdoxQxXktTjwNyDbEaj3n7jvl4s= 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= @@ -765,12 +440,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O 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.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc= -github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw= -github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs= -github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= -github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard v0.4.4 h1:53DncefIeLX3qEpjzlS1lyUmQoUEeOWPFWqaTJq9eAQ= +github.com/quasilyte/go-ruleguard v0.4.4/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= @@ -785,61 +456,23 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ 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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 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.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= -github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= -github.com/ryancurrah/gomodguard v1.3.1 h1:fH+fUg+ngsQO0ruZXXHnA/2aNllWA1whly4a6UvyzGE= -github.com/ryancurrah/gomodguard v1.3.1/go.mod h1:DGFHzEhi6iJ0oIDfMuo3TgrS+L9gZvrEfmjjuelnRU0= -github.com/ryancurrah/gomodguard v1.3.2 h1:CuG27ulzEB1Gu5Dk5gP8PFxSOZ3ptSdP5iI/3IXxM18= -github.com/ryancurrah/gomodguard v1.3.2/go.mod h1:LqdemiFomEjcxOqirbQCb3JFvSxH2JUYMerTFd3sF2o= -github.com/ryancurrah/gomodguard v1.3.3 h1:eiSQdJVNr9KTNxY2Niij8UReSwR8Xrte3exBrAZfqpg= -github.com/ryancurrah/gomodguard v1.3.3/go.mod h1:rsKQjj4l3LXe8N344Ow7agAy5p9yjsWOtRzUMYmA0QY= -github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= -github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= -github.com/ryanrolds/sqlclosecheck v0.4.0 h1:i8SX60Rppc1wRuyQjMciLqIzV3xnoHB7/tXbr6RGYNI= -github.com/ryanrolds/sqlclosecheck v0.4.0/go.mod h1:TBRRjzL31JONc9i4XMinicuo+s+E8yKZ5FN8X3G6CKQ= +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.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= -github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= 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/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/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.23.0 h1:01h+/2Kd+NblNItNeux0veSL5cBF1jbEOPrEhDzGYq0= -github.com/sashamelentyev/usestdlibvars v1.23.0/go.mod h1:YPwr/Y1LATzHI93CqoPUN/2BzGQ/6N/cl/KwgR0B/aU= -github.com/sashamelentyev/usestdlibvars v1.25.0 h1:IK8SI2QyFzy/2OD2PYnhy84dpfNo9qADrRt6LH8vSzU= -github.com/sashamelentyev/usestdlibvars v1.25.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/sashamelentyev/usestdlibvars v1.26.0 h1:LONR2hNVKxRmzIrZR0PhSF3mhCAzvnr+DcUiHgREfXE= -github.com/sashamelentyev/usestdlibvars v1.26.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= -github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.16.0 h1:Pi0JKoasQQ3NnoRao/ww/N/XdynIB9NRYYZT5CyOs5U= -github.com/securego/gosec/v2 v2.16.0/go.mod h1:xvLcVZqUfo4aAQu56TNv7/Ltz6emAOQAEsrZrt7uGlI= -github.com/securego/gosec/v2 v2.19.0 h1:gl5xMkOI0/E6Hxx0XCY2XujA3V7SNSefA8sC+3f1gnk= -github.com/securego/gosec/v2 v2.19.0/go.mod h1:hOkDcHz9J/XIgIlPDXalxjeVYsHxoWUc5zJSHxcB8YM= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9 h1:rnO6Zp1YMQwv8AyxzuwsVohljJgp4L0ZqiCgtACsPsc= -github.com/securego/gosec/v2 v2.20.1-0.20240525090044-5f0084eb01a9/go.mod h1:dg7lPlu/xK/Ut9SedURCoZbVCR4yC7fM65DtH9/CDHs= -github.com/securego/gosec/v2 v2.20.1-0.20240822074752-ab3f6c1c83a0 h1:VqD4JMoqwuuCz8GZlBDsIDyE6K4YUsWJpbNtuOWHoFk= -github.com/securego/gosec/v2 v2.20.1-0.20240822074752-ab3f6c1c83a0/go.mod h1:iyeMMRw8QEmueUSZ2VqmkQMiDyDcobfPnG00CV/NWdE= -github.com/securego/gosec/v2 v2.21.2 h1:deZp5zmYf3TWwU7A7cR2+SolbTpZ3HQiwFqnzQyEl3M= -github.com/securego/gosec/v2 v2.21.2/go.mod h1:au33kg78rNseF5PwPnTWhuYBFf534bvJRvOrgZ/bFzU= -github.com/securego/gosec/v2 v2.21.4 h1:Le8MSj0PDmOnHJgUATjD96PaXRvCpKC+DGJvwyy0Mlk= -github.com/securego/gosec/v2 v2.21.4/go.mod h1:Jtb/MwRQfRxCXyCm1rfM1BEiiiTfUOdyzzAhlr6lUTA= -github.com/securego/gosec/v2 v2.22.1 h1:IcBt3TpI5Y9VN1YlwjSpM2cHu0i3Iw52QM+PQeg7jN8= -github.com/securego/gosec/v2 v2.22.1/go.mod h1:4bb95X4Jz7VSEPdVjC0hD7C/yR6kdeUBvCPOy9gDQ0g= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= -github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc= +github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k= 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= @@ -849,37 +482,18 @@ 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/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8= -github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= -github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= -github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= -github.com/sivchari/tenv v1.10.0 h1:g/hzMA+dBCKqGXgW8AV/1xIWhAvDrx0zFKNR48NFMg0= -github.com/sivchari/tenv v1.10.0/go.mod h1:tdY24masnVoZFxYrHv/nD6Tc8FbkEtAQEEziXpyMgqY= -github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= -github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= -github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= -github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= 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.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 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.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -887,18 +501,14 @@ 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.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= -github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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= @@ -906,93 +516,41 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= -github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= -github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= -github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= -github.com/tdakkota/asciicheck v0.3.0 h1:LqDGgZdholxZMaJgpM6b0U9CFIjDCbFdUF00bDnBKOQ= -github.com/tdakkota/asciicheck v0.3.0/go.mod h1:KoJKXuX/Z/lt6XzLo8WMBfQGzak0SrAKZlvRr4tg8Ac= -github.com/tdakkota/asciicheck v0.4.0 h1:VZ13Itw4k1i7d+dpDSNS8Op645XgGHpkCEh/WHicgWw= -github.com/tdakkota/asciicheck v0.4.0/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= 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.4.11 h1:BVoBIqAf/2QdbFmSwAWnaIqDivZdOV0ZRwEm6jivLKw= -github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= -github.com/tetafro/godot v1.4.16 h1:4ChfhveiNLk4NveAZ9Pu2AN8QZ2nkUGFuadM9lrr5D0= -github.com/tetafro/godot v1.4.16/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/tetafro/godot v1.4.17 h1:pGzu+Ye7ZUEFx7LHU0dAKmCOXWsPjl7qA6iMGndsjPs= -github.com/tetafro/godot v1.4.17/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/tetafro/godot v1.4.20 h1:z/p8Ek55UdNvzt4TFn2zx2KscpW4rWqcnUrdmvWJj7E= -github.com/tetafro/godot v1.4.20/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= -github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= -github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= -github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= -github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= -github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= -github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= -github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= -github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= -github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQpIXDJRhQ= -github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= -github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs= -github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= -github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= -github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= -github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg= -github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= +github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= +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.11.0 h1:BJSt36snX9+4WTIXeJ7nvHBQBcm1h2SjQMSlmQ6aFSU= +github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU= 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.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA= -github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= -github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= 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.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= -github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= -github.com/ultraware/whitespace v0.1.0 h1:O1HKYoh0kIeqE8sFqZf1o0qbORXUCOQFrlaQyZsczZw= -github.com/ultraware/whitespace v0.1.0/go.mod h1:/se4r3beMFNmewJ4Xmz0nMQ941GJt+qmSHGP9emHYe0= -github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= -github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= 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.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y= -github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY= -github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= -github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= -github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= -github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= 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.3.0 h1:zwPch0fs9tdh9BmL5kcgSpvnObV+yHjO4JjVBl8IA10= -github.com/uudashr/iface v1.3.0/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= -github.com/xen0n/gosmopolitan v1.2.1 h1:3pttnTuFumELBRSh+KQs1zcz4fN6Zy7aB0xlnQSn1Iw= -github.com/xen0n/gosmopolitan v1.2.1/go.mod h1:JsHq/Brs1o050OOdmzHeOr0N7OtlnKRAGAsElF8xBQA= -github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= -github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +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.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= -github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= 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.2 h1:Um4P5RMmelfjQqQJKtE8ZW+dLZrXrENeIzWWKw800U4= -github.com/ykadowak/zerologlint v0.1.2/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= 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= @@ -1002,42 +560,21 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec 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.2.3 h1:gX4/RgK16ijY8V+BRQHAySfQAb354T7/xQpDB2n10P0= -gitlab.com/bosi/decorder v0.2.3/go.mod h1:9K1RB5+VPNQYtXtTDAzd2OEftsZb1oV0IrJrzChSdGE= -gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4= -gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA= 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.9.0 h1:Dzt6/tyP9ONr5g9h9P3cnYWCxeBFRkd0uJL/w+1Mxos= -go-simpler.org/musttag v0.9.0/go.mod h1:gA9nThnalvNSKpEoyp3Ko4/vCX2xTpqKoUtNqXOnVR4= -go-simpler.org/musttag v0.12.1 h1:yaMcjl/uyVnd1z6GqIhBiFH/PoqNN9f2IgtU7bp7W/0= -go-simpler.org/musttag v0.12.1/go.mod h1:46HKu04A3Am9Lne5kKP0ssgwY3AeIlqsDzz3UxKROpY= -go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= -go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= -go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= -go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= -go-simpler.org/sloglint v0.5.0 h1:2YCcd+YMuYpuqthCgubcF5lBSjb6berc5VMOYUHKrpY= -go-simpler.org/sloglint v0.5.0/go.mod h1:EUknX5s8iXqf18KQxKnaBHUPVriiPnOrPjjJcsaTcSQ= -go-simpler.org/sloglint v0.6.0 h1:0YcqSVG7LI9EVBfRPhgPec79BH6X6mwjFuUR5Mr7j1M= -go-simpler.org/sloglint v0.6.0/go.mod h1:+kJJtebtPePWyG5boFwY46COydAggADDOHM22zOvzBk= -go-simpler.org/sloglint v0.7.1 h1:qlGLiqHbN5islOxjeLXoPtUdZXb669RW+BDQ+xOSNoU= -go-simpler.org/sloglint v0.7.1/go.mod h1:OlaVDRh/FKKd4X4sIMbsz8st97vomydceL146Fthh/c= -go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= -go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= -go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= -go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= +go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU= +go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= +go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= +go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= +go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk= +go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s= 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.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.tmz.dev/musttag v0.7.0 h1:QfytzjTWGXZmChoX0L++7uQN+yRCPfyFm+whsM+lfGc= -go.tmz.dev/musttag v0.7.0/go.mod h1:oTFPvgOkJmp5kYL02S8+jrH0eLrBIl57rzWeA26zDEM= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= 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.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= @@ -1050,10 +587,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U 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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 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= @@ -1066,24 +600,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 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-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= -golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= -golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 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-20230224173230-c95f2b4c22f2 h1:J74nGeMgeFnYQJN59eFwh06jX/V8g0lB7LWpjSLxtgU= -golang.org/x/exp/typeparams v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= -golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f h1:WTyX8eCCyfdqiPYkRGm0MqElSfYFH3yR1+rl/mct9sA= -golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -1098,7 +617,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl 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/lint v0.0.0-20201208152925-83fdc39ff7b5/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= @@ -1107,34 +625,17 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB 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.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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 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.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +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-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= @@ -1164,17 +665,12 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R 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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -1185,10 +681,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr 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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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= @@ -1203,20 +695,10 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ 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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -1248,17 +730,11 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/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-20210423185535-09eb48e85fd7/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= @@ -1267,38 +743,19 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc 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-20220702020025-31831981b65f/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/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 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= @@ -1309,28 +766,15 @@ 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.4/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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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= @@ -1340,7 +784,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 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-20190321232350-e250d351ecad/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= @@ -1348,10 +791,8 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw 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-20190910044552-dd2b5c81c578/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-20191108193012-7d206e10da11/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= @@ -1381,44 +822,20 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 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= @@ -1439,16 +856,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M 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/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 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/appengine v1.6.7/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= @@ -1478,13 +891,6 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc 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/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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= @@ -1497,10 +903,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa 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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 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= @@ -1513,14 +915,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= @@ -1545,32 +941,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh 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.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw= -honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= -honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= -honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= -honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= -honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= -honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= -mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= -mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= -mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= -mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d h1:3rvTIIM22r9pvXk+q3swxUQAQOxksVMGK7sml4nG57w= -mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RFiZMMsLVL+A96Nvptar8Fj71is= -mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14 h1:zCr3iRRgdk5eIikZNDphGcM6KGVTx3Yu+/Uu9Es254w= -mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= -mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1 h1:Nykk7fggxChwLK4rUPYESzeIwqsuxXXlFEAh5YhaMRo= -mvdan.cc/unparam v0.0.0-20240427195214-063aff900ca1/go.mod h1:ZzZjEpJDOmx8TdVU6umamY3Xy0UAQUI2DHbf05USVbI= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= -mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= +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.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k= +mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg= +mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8= +mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE= 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= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file diff --git a/.bingo/helm.mod b/.bingo/helm.mod new file mode 100644 index 0000000000..5c54ed4210 --- /dev/null +++ b/.bingo/helm.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.24.3 + +require helm.sh/helm/v3 v3.18.4 // cmd/helm diff --git a/.bingo/helm.sum b/.bingo/helm.sum new file mode 100644 index 0000000000..4477f0392d --- /dev/null +++ b/.bingo/helm.sum @@ -0,0 +1,303 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= +github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +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/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/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.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +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/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220811171246-fbc7d0a398ab/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.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.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.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= +helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= +helm.sh/helm/v3 v3.18.4/go.mod h1:WVnwKARAw01iEdjpEkP7Ii1tT1pTPYfM1HsakFKM3LI= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= +k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= +k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/.bingo/kind.mod b/.bingo/kind.mod index 3037dbbaaa..becf46c7b7 100644 --- a/.bingo/kind.mod +++ b/.bingo/kind.mod @@ -2,4 +2,4 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT go 1.20 -require sigs.k8s.io/kind v0.27.0 +require sigs.k8s.io/kind v0.30.0 diff --git a/.bingo/kind.sum b/.bingo/kind.sum index b4115fe16f..af37989b32 100644 --- a/.bingo/kind.sum +++ b/.bingo/kind.sum @@ -42,6 +42,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= @@ -64,6 +66,10 @@ sigs.k8s.io/kind v0.26.0 h1:8fS6I0Q5WGlmLprSpH0DarlOSdcsv0txnwc93J2BP7M= sigs.k8s.io/kind v0.26.0/go.mod h1:t7ueEpzPYJvHA8aeLtI52rtFftNgUYUaCwvxjk7phfw= sigs.k8s.io/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA= sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY= +sigs.k8s.io/kind v0.29.0 h1:3TpCsyh908IkXXpcSnsMjWdwdWjIl7o9IMZImZCWFnI= +sigs.k8s.io/kind v0.29.0/go.mod h1:ldWQisw2NYyM6k64o/tkZng/1qQW7OlzcN5a8geJX3o= +sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= +sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/.bingo/setup-envtest.mod b/.bingo/setup-envtest.mod index f9db6da220..0a366239fc 100644 --- a/.bingo/setup-envtest.mod +++ b/.bingo/setup-envtest.mod @@ -1,7 +1,7 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.23.0 +go 1.24.0 -toolchain go1.23.4 +toolchain go1.24.3 -require sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250304084143-6eb011f4f89e +require sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250620151452-b9a9ca01fd37 diff --git a/.bingo/setup-envtest.sum b/.bingo/setup-envtest.sum index 93710bfa46..dad3e24e86 100644 --- a/.bingo/setup-envtest.sum +++ b/.bingo/setup-envtest.sum @@ -73,6 +73,8 @@ golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -99,5 +101,7 @@ sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250226022829-9d8d219 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250226022829-9d8d219840a4/go.mod h1:QXw4XLB4ayZHsgXTf7cdyGzacNz9KQsdiI6apU+K07E= sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250304084143-6eb011f4f89e h1:ezClPOTx54T3hRw/3eNMYr5LKzikTvQ380UuGy/X/Co= sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250304084143-6eb011f4f89e/go.mod h1:QXw4XLB4ayZHsgXTf7cdyGzacNz9KQsdiI6apU+K07E= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250620151452-b9a9ca01fd37 h1:NSnbH7C6/fYc5L3FxMQiSlFBqYi+32LnFsXwArzOlIM= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250620151452-b9a9ca01fd37/go.mod h1:zCcqn1oG9844T8/vZSYcnqOyoEmTHro4bliTJI6j4OY= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/.bingo/variables.env b/.bingo/variables.env index 07e40961fa..64c92fc14b 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -10,17 +10,21 @@ fi BINGO="${GOBIN}/bingo-v0.9.0" -CONTROLLER_GEN="${GOBIN}/controller-gen-v0.17.3" +CONTROLLER_GEN="${GOBIN}/controller-gen-v0.19.0" CRD_DIFF="${GOBIN}/crd-diff-v0.2.0" CRD_REF_DOCS="${GOBIN}/crd-ref-docs-v0.1.0" -GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.64.6" +GOJQ="${GOBIN}/gojq-v0.12.17" + +GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.1.6" GORELEASER="${GOBIN}/goreleaser-v1.26.2" -KIND="${GOBIN}/kind-v0.27.0" +HELM="${GOBIN}/helm-v3.18.4" + +KIND="${GOBIN}/kind-v0.30.0" KUSTOMIZE="${GOBIN}/kustomize-v5.6.0" @@ -28,5 +32,7 @@ OPERATOR_SDK="${GOBIN}/operator-sdk-v1.39.1" OPM="${GOBIN}/opm-v1.51.0" -SETUP_ENVTEST="${GOBIN}/setup-envtest-v0.0.0-20250304084143-6eb011f4f89e" +SETUP_ENVTEST="${GOBIN}/setup-envtest-v0.0.0-20250620151452-b9a9ca01fd37" + +YAMLFMT="${GOBIN}/yamlfmt-v0.20.0" diff --git a/.bingo/yamlfmt.mod b/.bingo/yamlfmt.mod new file mode 100644 index 0000000000..152ea9ecb9 --- /dev/null +++ b/.bingo/yamlfmt.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.24.4 + +require github.com/google/yamlfmt v0.20.0 // cmd/yamlfmt diff --git a/.bingo/yamlfmt.sum b/.bingo/yamlfmt.sum new file mode 100644 index 0000000000..e9450c1919 --- /dev/null +++ b/.bingo/yamlfmt.sum @@ -0,0 +1,16 @@ +github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= +github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/yamlfmt v0.20.0 h1:EfMuMFEZGnXPn2NY+KgJTH9sNs6P+am/Of6IwE2K6P8= +github.com/google/yamlfmt v0.20.0/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.github/OWNERS b/.github/OWNERS new file mode 100644 index 0000000000..835cabe507 --- /dev/null +++ b/.github/OWNERS @@ -0,0 +1,2 @@ +approvers: + - ci-approvers diff --git a/.github/workflows/crd-diff.yaml b/.github/workflows/crd-diff.yaml index 5b1b2e7f86..3bb66d293a 100644 --- a/.github/workflows/crd-diff.yaml +++ b/.github/workflows/crd-diff.yaml @@ -5,11 +5,11 @@ jobs: crd-diff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 70ec12b046..f5e1c109ff 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -12,56 +12,102 @@ jobs: extension-developer-e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run the extension developer e2e test - run: make extension-developer-e2e + run: make test-extension-developer-e2e e2e-kind: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run e2e tests - run: ARTIFACT_PATH=/tmp/artifacts make test-e2e + run: ARTIFACT_PATH=/tmp/artifacts E2E_SUMMARY_OUTPUT=$GITHUB_STEP_SUMMARY make test-e2e - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: failure() with: name: e2e-artifacts path: /tmp/artifacts/ - - uses: codecov/codecov-action@v5.4.3 + - uses: codecov/codecov-action@v5.5.1 with: disable_search: true files: coverage/e2e.out flags: e2e token: ${{ secrets.CODECOV_TOKEN }} + experimental-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run e2e tests + run: ARTIFACT_PATH=/tmp/artifacts E2E_SUMMARY_OUTPUT=$GITHUB_STEP_SUMMARY make test-experimental-e2e + + - uses: actions/upload-artifact@v5 + if: failure() + with: + name: experimental-e2e-artifacts + path: /tmp/artifacts/ + + - uses: codecov/codecov-action@v5.5.1 + with: + disable_search: true + files: coverage/experimental-e2e.out + flags: experimental-e2e + token: ${{ secrets.CODECOV_TOKEN }} + upgrade-e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run the upgrade e2e test run: ARTIFACT_PATH=/tmp/artifacts make test-upgrade-e2e - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v5 if: failure() with: name: upgrade-e2e-artifacts path: /tmp/artifacts/ + + upgrade-experimental-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run the upgrade e2e test + run: ARTIFACT_PATH=/tmp/artifacts make test-upgrade-experimental-e2e + + - uses: actions/upload-artifact@v5 + if: failure() + with: + name: upgrade-experimental-e2e-artifacts + path: /tmp/artifacts/ + diff --git a/.github/workflows/go-apidiff.yaml b/.github/workflows/go-apidiff.yaml index f4061342f7..44d53621cd 100644 --- a/.github/workflows/go-apidiff.yaml +++ b/.github/workflows/go-apidiff.yaml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/go-verdiff.yaml b/.github/workflows/go-verdiff.yaml index 2cc662ab24..82b0d201cd 100644 --- a/.github/workflows/go-verdiff.yaml +++ b/.github/workflows/go-verdiff.yaml @@ -7,7 +7,7 @@ jobs: go-verdiff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Check golang version diff --git a/.github/workflows/pages.yaml b/.github/workflows/pages.yaml index 65b70dcf2d..b757aec9ea 100644 --- a/.github/workflows/pages.yaml +++ b/.github/workflows/pages.yaml @@ -30,9 +30,9 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f9a8d79359..bd472f578d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,12 +18,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code into the Go module directory - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" diff --git a/.github/workflows/sanity.yaml b/.github/workflows/sanity.yaml index 601fbd14da..fde951310c 100644 --- a/.github/workflows/sanity.yaml +++ b/.github/workflows/sanity.yaml @@ -12,9 +12,9 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Run verification checks @@ -22,11 +22,23 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Run golangci linting checks - run: make lint GOLANGCI_LINT_ARGS="--out-format colored-line-number" + run: make lint + + lint-helm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + + - name: Run helm linting checks + run: make lint-helm diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..95cb101668 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,55 @@ +# This workflow automatically marks issues and pull requests as stale after 90 days of inactivity +# and closes them after an additional 30 days if no further activity occurs. +# +# Key behavior: +# - After 90 days of no activity: +# - Open issues and pull requests are labeled with "lifecycle/stale" +# - A comment is posted to notify contributors about the inactivity +# +# - After 30 additional days (i.e., 120 days total): +# - If still inactive and still labeled "lifecycle/stale", the issue or PR is closed +# - A closing comment is posted to explain why it was closed +# +# - Activity such as a comment, commit, or label removal during the stale period +# will remove the "lifecycle/stale" label and reset the clock +# +# - Items with any of the following labels will never be marked stale or closed: +# - security +# - planned +# - priority/critical +# - lifecycle/frozen +# - verified +# +# This workflow uses: https://github.com/actions/stale +name: "Close stale issues and PRs" +on: + schedule: + - cron: "0 1 * * *" # Runs daily at 01:00 UTC + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + # allow labeling, commenting, closing issues and PRs + issues: write + pull-requests: write + steps: + - uses: actions/stale@v10 + 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/test-regression.yaml b/.github/workflows/test-regression.yaml new file mode 100644 index 0000000000..3b09074af2 --- /dev/null +++ b/.github/workflows/test-regression.yaml @@ -0,0 +1,31 @@ +name: test-regression + +on: + workflow_dispatch: + pull_request: + merge_group: + push: + branches: + - main + +jobs: + test-regression: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run regression tests + run: | + make test-regression + + - uses: codecov/codecov-action@v5.5.1 + with: + disable_search: true + files: coverage/regression.out + flags: unit + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/tilt.yaml b/.github/workflows/tilt.yaml index 63ddc2a13c..858250a132 100644 --- a/.github/workflows/tilt.yaml +++ b/.github/workflows/tilt.yaml @@ -17,11 +17,11 @@ jobs: tilt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: path: operator-controller - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: "operator-controller/go.mod" - name: Install Tilt diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 331b910403..0579b1219d 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -23,7 +23,7 @@ jobs: run: | make test-unit - - uses: codecov/codecov-action@v5.4.3 + - uses: codecov/codecov-action@v5.5.1 with: disable_search: true files: coverage/unit.out diff --git a/.github/workflows/catalogd-demo.yaml b/.github/workflows/update-demos.yaml similarity index 50% rename from .github/workflows/catalogd-demo.yaml rename to .github/workflows/update-demos.yaml index c731576482..29d6fd4c47 100644 --- a/.github/workflows/catalogd-demo.yaml +++ b/.github/workflows/update-demos.yaml @@ -1,13 +1,22 @@ -name: catalogd-demo +name: update-demos on: + schedule: + - cron: '0 3 * * *' # Runs every day at 03:00 UTC workflow_dispatch: - merge_group: - pull_request: push: - branches: - - main - + paths: + - 'api/*' + - 'config/*' + - 'hack/demo/*' + - '.github/workflows/update-demos.yaml' + pull_request: + paths: + - 'api/*' + - 'config/*' + - 'hack/demo/*' + - '.github/workflows/update-demos.yaml' + jobs: demo: runs-on: ubuntu-latest @@ -15,8 +24,8 @@ jobs: TERM: linux steps: - run: sudo apt update && sudo apt install -y asciinema curl - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Run Demo Update @@ -26,5 +35,5 @@ jobs: PATH="$PATH" \ TERM="xterm-256color" \ SHELL="/bin/bash" \ - make demo-update + make update-demos diff --git a/.gitignore b/.gitignore index 1bd3455239..abd509dafb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,12 @@ coverage cover.out # Release output -dist/** -operator-controller.yaml -install.sh -catalogd.yaml +/dist/** +/operator-controller.yaml +/operator-controller-experimental.yaml +/default-catalogs.yaml +/install.sh +/install-experimental.sh # vendored files vendor/ @@ -36,9 +38,15 @@ vendor/ \#*\# .\#* +# AI temp files files +.claude/ + # documentation website asset folder site .tiltbuild/ .catalogd-tmp/ .vscode + +# Temporary files and directories +/test/regression/convert/testdata/tmp/* diff --git a/.golangci.yaml b/.golangci.yaml index 7f64bc040a..e4ba57da9c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,75 +1,74 @@ -######## -# NOTE -# -# This file is duplicated in the following repos: -# - operator-framework/kubectl-operator -# - operator-framework/catalogd -# - operator-framework/operator-controller -# -# If you are making a change, please make it in ALL -# of the above repositories! -# -# TODO: Find a way to have a shared golangci config. -######## - -run: - # Default timeout is 1m, up to give more room - timeout: 4m - -linters: - enable: - - asciicheck - - bodyclose - - errorlint - - gci - - gofmt - - govet - - gosec - - importas - - misspell - - nestif - - nonamedreturns - - prealloc - - stylecheck - - testifylint - - tparallel - - unconvert - - unparam - - unused - - whitespace - -linters-settings: - gci: - sections: - - standard - - dot - - default - - prefix(github.com/operator-framework) - - localmodule - custom-order: true - - 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/operator-controller/internal/util/([^/]+)$" - alias: "${1}util" - +version: "2" output: formats: - - format: tab + 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/operator-controller/internal/util/([^/]+)$ + alias: ${1}util + 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) + - localmodule + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index d269b20d00..7200142142 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -124,9 +124,11 @@ release: disable: '{{ ne .Env.ENABLE_RELEASE_PIPELINE "true" }}' mode: replace extra_files: - - glob: 'operator-controller.yaml' - - glob: './config/catalogs/clustercatalogs/default-catalogs.yaml' - - glob: 'install.sh' + - glob: '{{ .Env.STANDARD_RELEASE_MANIFEST }}' + - glob: '{{ .Env.STANDARD_RELEASE_INSTALL }}' + - glob: '{{ .Env.EXPERIMENTAL_RELEASE_MANIFEST }}' + - glob: '{{ .Env.EXPERIMENTAL_RELEASE_INSTALL }}' + - glob: '{{ .Env.RELEASE_CATALOGS }}' header: | ## Installation diff --git a/.tilt-support b/.tilt-support index c55d2851df..9cb01b1526 100644 --- a/.tilt-support +++ b/.tilt-support @@ -5,7 +5,7 @@ load('ext://cert_manager', 'deploy_cert_manager') def deploy_cert_manager_if_needed(): cert_manager_var = '__CERT_MANAGER__' if os.getenv(cert_manager_var) != '1': - deploy_cert_manager(version="v1.15.3") + deploy_cert_manager(version="v1.18.2") os.putenv(cert_manager_var, '1') @@ -14,7 +14,7 @@ def deploy_cert_manager_if_needed(): docker_build( ref='helper', context='.', - build_args={'GO_VERSION': '1.23'}, + build_args={'GO_VERSION': '1.24'}, dockerfile_contents=''' ARG GO_VERSION FROM golang:${GO_VERSION} @@ -67,6 +67,7 @@ COPY {} / live_update=[ sync('.tiltbuild/bin/{}'.format(binary_name), '/{}'.format(binary_name)), ], + restart_file="/.tilt_restart_proc", # The command to run in the container. entrypoint=entrypoint, ) @@ -129,23 +130,24 @@ def process_yaml(yaml): # data format: # { -# 'image': 'quay.io/operator-framework/rukpak', -# 'yaml': 'manifests/overlays/cert-manager', -# 'binaries': { -# 'core': 'core', -# 'crdvalidator': 'crd-validation-webhook', -# 'helm': 'helm-provisioner', -# 'webhooks': 'rukpak-webhooks', +# 'repos': { +# 'catalogd': { +# 'image': 'quay.io/operator-framework/catalogd', +# 'binary': './cmd/catalogd', +# 'deployment': 'catalogd-controller-manager', +# 'deps': ['api', 'cmd/catalogd', 'internal/catalogd', 'internal/shared', 'go.mod', 'go.sum'], +# 'starting_debug_port': 20000, +# }, +# ... additional entries here ... # }, -# 'deps': ['api', 'cmd/binary_name', 'internal', 'pkg'], -# }, -def deploy_repo(repo, data, tags="", debug=True): - print('Deploying repo {}'.format(repo)) - deploy_cert_manager_if_needed() +# 'yaml': 'config/overlays/tilt-local-dev', +# } - local_port = data['starting_debug_port'] - for binary, deployment in data['binaries'].items(): - build_binary(repo, binary, data['deps'], data['image'], tags, debug) - k8s_resource(deployment, port_forwards=['{}:30000'.format(local_port)]) - local_port += 1 - process_yaml(kustomize(data['yaml'])) +def deploy_repo(data, tags="", debug=True): + deploy_cert_manager_if_needed() + for reponame, repo in data['repos'].items(): + print('Deploying repo {}'.format(reponame)) + local_port = repo['starting_debug_port'] + build_binary(reponame, repo['binary'], repo['deps'], repo['image'], tags, debug) + k8s_resource(repo['deployment'], port_forwards=['{}:30000'.format(local_port)]) + process_yaml(helm('helm/olmv1', name="olmv1", values=[data['yaml']])) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e67cd25cb..156ae32e62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -151,7 +151,7 @@ Please follow this style to make the operator-controller project easier to revie ### Go version -Our goal is to minimize disruption by requiring the lowest possible Go language version. This means avoiding updaties to the go version specified in [go.mod](go.mod) (and other locations). +Our goal is to minimize disruption by requiring the lowest possible Go language version. This means avoiding updaties to the go version specified in the project's `go.mod` file (and other locations). There is a GitHub PR CI job named `go-verdiff` that will inform a PR author if the Go language version has been updated. It is not a required test, but failures should prompt authors and reviewers to have a discussion with the community about the Go language version change. diff --git a/Makefile b/Makefile index 6245c6ccb5..379599d92f 100644 --- a/Makefile +++ b/Makefile @@ -40,14 +40,8 @@ endif # Ensure ENVTEST_VERSION follows correct "X.Y.x" format ENVTEST_VERSION := $(K8S_VERSION).x -# Not guaranteed to have patch releases available and node image tags are full versions (i.e v1.28.0 - no v1.28, v1.29, etc.) -# The K8S_VERSION is set by getting the version of the k8s.io/client-go dependency from the go.mod -# and sets major version to "1" and the patch version to "0". For example, a client-go version of v0.28.5 -# will map to a K8S_VERSION of 1.28.0 -KIND_CLUSTER_IMAGE := kindest/node:v$(K8S_VERSION).0 - # Define dependency versions (use go.mod if we also use Go code from dependency) -export CERT_MGR_VERSION := v1.17.1 +export CERT_MGR_VERSION := v1.18.2 export WAIT_TIMEOUT := 60s # Install default ClusterCatalogs @@ -77,7 +71,19 @@ else $(warning Could not find docker or podman in path! This may result in targets requiring a container runtime failing!) endif -KUSTOMIZE_BUILD_DIR := config/overlays/cert-manager +export STANDARD_RELEASE_MANIFEST := operator-controller.yaml +export STANDARD_RELEASE_INSTALL := install.sh +export EXPERIMENTAL_RELEASE_MANIFEST := operator-controller-experimental.yaml +export EXPERIMENTAL_RELEASE_INSTALL := install-experimental.sh +export RELEASE_CATALOGS := default-catalogs.yaml + +# List of manifests that are checked in +MANIFEST_HOME := manifests +STANDARD_MANIFEST := $(MANIFEST_HOME)/standard.yaml +STANDARD_E2E_MANIFEST := $(MANIFEST_HOME)/standard-e2e.yaml +EXPERIMENTAL_MANIFEST := $(MANIFEST_HOME)/experimental.yaml +EXPERIMENTAL_E2E_MANIFEST := $(MANIFEST_HOME)/experimental-e2e.yaml +CATALOGS_MANIFEST := $(MANIFEST_HOME)/default-catalogs.yaml # Disable -j flag for make .NOTPARALLEL: @@ -100,7 +106,7 @@ KUSTOMIZE_BUILD_DIR := config/overlays/cert-manager .PHONY: help help: #HELP Display essential help. - @awk 'BEGIN {FS = ":[^#]*#HELP"; printf "\nUsage:\n make \033[36m\033[0m\n\n"} /^[a-zA-Z_0-9-]+:.*#HELP / { printf " \033[36m%-17s\033[0m %s\n", $$1, $$2 } ' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":[^#]*#HELP"; printf "\nUsage:\n make \033[36m\033[0m\n\n"} /^[a-zA-Z_0-9-]+:.*#HELP / { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } ' $(MAKEFILE_LIST) .PHONY: help-extended help-extended: #HELP Display extended help. @@ -112,6 +118,10 @@ help-extended: #HELP Display extended help. lint: lint-custom $(GOLANGCI_LINT) #HELP Run golangci linter. $(GOLANGCI_LINT) run --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS) +lint-helm: $(HELM) #HELP Run helm linter + helm lint helm/olmv1 + helm lint helm/prometheus + .PHONY: custom-linter-build custom-linter-build: #EXHELP Build custom linter go build -tags $(GO_BUILD_TAGS) -o ./bin/custom-linter ./hack/ci/custom-linters/cmd @@ -128,61 +138,67 @@ k8s-pin: #EXHELP Pin k8s staging modules based on k8s.io/kubernetes version (in tidy: go mod tidy -.PHONY: manifests -KUSTOMIZE_CATD_CRDS_DIR := config/base/catalogd/crd/bases -KUSTOMIZE_CATD_RBAC_DIR := config/base/catalogd/rbac -KUSTOMIZE_CATD_WEBHOOKS_DIR := config/base/catalogd/manager/webhook -KUSTOMIZE_OPCON_CRDS_DIR := config/base/operator-controller/crd/bases -KUSTOMIZE_OPCON_RBAC_DIR := config/base/operator-controller/rbac -CRD_WORKING_DIR := crd_work_dir # Due to https://github.com/kubernetes-sigs/controller-tools/issues/837 we can't specify individual files # So we have to generate them together and then move them into place -manifests: $(CONTROLLER_GEN) #EXHELP Generate WebhookConfiguration, ClusterRole, and CustomResourceDefinition objects. - mkdir $(CRD_WORKING_DIR) - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) crd paths="./api/v1/..." output:crd:artifacts:config=$(CRD_WORKING_DIR) - mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clusterextensions.yaml $(KUSTOMIZE_OPCON_CRDS_DIR) - mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clustercatalogs.yaml $(KUSTOMIZE_CATD_CRDS_DIR) - rmdir $(CRD_WORKING_DIR) - # Generate the remaining operator-controller manifests - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR) - # Generate the remaining catalogd manifests - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) rbac:roleName=manager-role paths="./internal/catalogd/..." output:rbac:artifacts:config=$(KUSTOMIZE_CATD_RBAC_DIR) - $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) webhook paths="./internal/catalogd/..." output:webhook:artifacts:config=$(KUSTOMIZE_CATD_WEBHOOKS_DIR) +.PHONY: update-crds +update-crds: + hack/tools/update-crds.sh + +# The filename variables can be overridden on the command line if you want to change the set of values files: +# e.g. make "manifests/standard.yaml=helm/cert-manager.yaml my-values-file.yaml" manifests +# +# The set of MANIFESTS to be generated can be changed; you can generate your own custom manifest +# e.g. make MANIFESTS=test.yaml "test.yaml=helm/e2e.yaml" manifests +# +# Override HELM_SETTINGS on the command line to include additional Helm settings +# e.g. make HELM_SETTINGS="options.openshift.enabled=true" manifests +# e.g. make HELM_SETTINGS="options.operatorController.features.enabled={WebhookProviderCertManager}" manifests +# +MANIFESTS ?= $(STANDARD_MANIFEST) $(STANDARD_E2E_MANIFEST) $(EXPERIMENTAL_MANIFEST) $(EXPERIMENTAL_E2E_MANIFEST) +$(STANDARD_MANIFEST) ?= helm/cert-manager.yaml +$(STANDARD_E2E_MANIFEST) ?= helm/cert-manager.yaml helm/e2e.yaml +$(EXPERIMENTAL_MANIFEST) ?= helm/cert-manager.yaml helm/experimental.yaml +$(EXPERIMENTAL_E2E_MANIFEST) ?= helm/cert-manager.yaml helm/experimental.yaml helm/e2e.yaml +HELM_SETTINGS ?= +.PHONY: $(MANIFESTS) +$(MANIFESTS): $(HELM) + @mkdir -p $(MANIFEST_HOME) + $(HELM) template olmv1 helm/olmv1 $(addprefix --values ,$($@)) $(addprefix --set ,$(HELM_SETTINGS)) > $@ + +# Generate manifests stored in source-control +.PHONY: manifests +manifests: update-crds $(MANIFESTS) $(HELM) #EXHELP Generate OLMv1 manifests + # These are testing existing manifest options without saving the results + $(HELM) template olmv1 helm/olmv1 --values helm/tilt.yaml $(addprefix --set ,$(HELM_SETTINGS)) > /dev/null + $(HELM) template olmv1 helm/olmv1 --set "options.openshift.enabled=true" > /dev/null .PHONY: generate generate: $(CONTROLLER_GEN) #EXHELP Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. + @find api cmd hack internal -name "zz_generated.deepcopy.go" -delete # Need to delete the files for them to be generated properly $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: verify -verify: k8s-pin fmt generate manifests crd-ref-docs generate-test-data #HELP Verify all generated code is up-to-date. Runs k8s-pin instead of just tidy. +verify: k8s-pin kind-verify-versions fmt generate manifests update-tls-profiles crd-ref-docs #HELP Verify all generated code is up-to-date. Runs k8s-pin instead of just tidy. git diff --exit-code -# Renders registry+v1 bundles in test/convert -# Used by CI in verify to catch regressions in the registry+v1 -> plain conversion code -.PHONY: generate-test-data -generate-test-data: - go run test/convert/generate-manifests.go - .PHONY: fix-lint fix-lint: $(GOLANGCI_LINT) #EXHELP Fix lint issues $(GOLANGCI_LINT) run --fix --build-tags $(GO_BUILD_TAGS) $(GOLANGCI_LINT_ARGS) .PHONY: fmt -fmt: #EXHELP Formats code +fmt: $(YAMLFMT) #EXHELP Formats code go fmt ./... + $(YAMLFMT) -gitignore_excludes testdata -.PHONY: bingo-upgrade -bingo-upgrade: $(BINGO) #EXHELP Upgrade tools - @for pkg in $$($(BINGO) list | awk '{ print $$3 }' | tail -n +3 | sed 's/@.*//'); do \ - echo -e "Upgrading \033[35m$$pkg\033[0m to latest..."; \ - $(BINGO) get "$$pkg@latest"; \ - done +.PHONY: update-tls-profiles +update-tls-profiles: $(GOJQ) #EXHELP Update TLS profiles from the Mozilla wiki + env JQ=$(GOJQ) hack/tools/update-tls-profiles.sh .PHONY: verify-crd-compatibility CRD_DIFF_ORIGINAL_REF := git://main?path= CRD_DIFF_UPDATED_REF := file:// -CRD_DIFF_OPCON_SOURCE := config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml -CRD_DIFF_CATD_SOURCE := config/base/catalogd/crd/bases/olm.operatorframework.io_clustercatalogs.yaml +CRD_DIFF_OPCON_SOURCE := helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml +CRD_DIFF_CATD_SOURCE := helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml CRD_DIFF_CONFIG := crd-diff-config.yaml verify-crd-compatibility: $(CRD_DIFF) manifests $(CRD_DIFF) --config="${CRD_DIFF_CONFIG}" "${CRD_DIFF_ORIGINAL_REF}${CRD_DIFF_OPCON_SOURCE}" ${CRD_DIFF_UPDATED_REF}${CRD_DIFF_OPCON_SOURCE} @@ -191,27 +207,31 @@ verify-crd-compatibility: $(CRD_DIFF) manifests #SECTION Test .PHONY: test -test: manifests generate fmt lint test-unit test-e2e #HELP Run all tests. +test: manifests generate fmt lint test-unit test-e2e test-regression #HELP Run all tests. .PHONY: e2e e2e: #EXHELP Run the e2e tests. go test -count=1 -v ./test/e2e/... +.PHONY: experimental-e2e +experimental-e2e: #EXHELP Run the experimental e2e tests. + go test -count=1 -v ./test/experimental-e2e/... + E2E_REGISTRY_NAME := docker-registry E2E_REGISTRY_NAMESPACE := operator-controller-e2e export REG_PKG_NAME := registry-operator -export LOCAL_REGISTRY_HOST := $(E2E_REGISTRY_NAME).$(E2E_REGISTRY_NAMESPACE).svc:5000 -export CLUSTER_REGISTRY_HOST := localhost:30000 +export CLUSTER_REGISTRY_HOST := $(E2E_REGISTRY_NAME).$(E2E_REGISTRY_NAMESPACE).svc:5000 +export LOCAL_REGISTRY_HOST := localhost:30000 export E2E_TEST_CATALOG_V1 := e2e/test-catalog:v1 export E2E_TEST_CATALOG_V2 := e2e/test-catalog:v2 -export CATALOG_IMG := $(LOCAL_REGISTRY_HOST)/$(E2E_TEST_CATALOG_V1) -.PHONY: test-ext-dev-e2e -test-ext-dev-e2e: $(OPERATOR_SDK) $(KUSTOMIZE) $(KIND) #HELP Run extension create, upgrade and delete tests. - test/extension-developer-e2e/setup.sh $(OPERATOR_SDK) $(CONTAINER_RUNTIME) $(KUSTOMIZE) $(KIND) $(KIND_CLUSTER_NAME) $(E2E_REGISTRY_NAMESPACE) +export CATALOG_IMG := $(CLUSTER_REGISTRY_HOST)/$(E2E_TEST_CATALOG_V1) +.PHONY: extension-developer-e2e +extension-developer-e2e: $(OPERATOR_SDK) $(KUSTOMIZE) #EXHELP Run extension create, upgrade and delete tests. + test/extension-developer-e2e/setup.sh $(OPERATOR_SDK) $(CONTAINER_RUNTIME) $(KUSTOMIZE) ${LOCAL_REGISTRY_HOST} ${CLUSTER_REGISTRY_HOST} go test -count=1 -v ./test/extension-developer-e2e/... -UNIT_TEST_DIRS := $(shell go list ./... | grep -v /test/) +UNIT_TEST_DIRS := $(shell go list ./... | grep -vE "/test/|/testutils") COVERAGE_UNIT_DIR := $(ROOT_DIR)/coverage/unit .PHONY: envtest-k8s-bins #HELP Uses setup-envtest to download and install the binaries required to run ENVTEST-test based locally at the project/bin directory. @@ -230,12 +250,17 @@ test-unit: $(SETUP_ENVTEST) envtest-k8s-bins #HELP Run the unit tests $(UNIT_TEST_DIRS) \ -test.gocoverdir=$(COVERAGE_UNIT_DIR) +COVERAGE_REGRESSION_DIR := $(ROOT_DIR)/coverage/regression +.PHONY: test-regression +test-regression: #HELP Run regression test + rm -rf $(COVERAGE_REGRESSION_DIR) && mkdir -p $(COVERAGE_REGRESSION_DIR) + go test -count=1 -v ./test/regression/... -cover -coverprofile ${ROOT_DIR}/coverage/regression.out -test.gocoverdir=$(COVERAGE_REGRESSION_DIR) + .PHONY: image-registry E2E_REGISTRY_IMAGE=localhost/e2e-test-registry:devel image-registry: export GOOS=linux image-registry: export GOARCH=amd64 image-registry: ## Build the testdata catalog used for e2e tests and push it to the image registry - go build $(GO_BUILD_FLAGS) $(GO_BUILD_EXTRA_FLAGS) -tags '$(GO_BUILD_TAGS)' -ldflags '$(GO_BUILD_LDFLAGS)' -gcflags '$(GO_BUILD_GCFLAGS)' -asmflags '$(GO_BUILD_ASMFLAGS)' -o ./testdata/registry/bin/registry ./testdata/registry/registry.go go build $(GO_BUILD_FLAGS) $(GO_BUILD_EXTRA_FLAGS) -tags '$(GO_BUILD_TAGS)' -ldflags '$(GO_BUILD_LDFLAGS)' -gcflags '$(GO_BUILD_GCFLAGS)' -asmflags '$(GO_BUILD_ASMFLAGS)' -o ./testdata/push/bin/push ./testdata/push/push.go $(CONTAINER_RUNTIME) build -f ./testdata/Dockerfile -t $(E2E_REGISTRY_IMAGE) ./testdata $(CONTAINER_RUNTIME) save $(E2E_REGISTRY_IMAGE) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) @@ -247,20 +272,37 @@ image-registry: ## Build the testdata catalog used for e2e tests and push it to # # for example: ARTIFACT_PATH=/tmp/artifacts make test-e2e .PHONY: test-e2e +test-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) test-e2e: KIND_CLUSTER_NAME := operator-controller-e2e -test-e2e: KUSTOMIZE_BUILD_DIR := config/overlays/e2e test-e2e: GO_BUILD_EXTRA_FLAGS := -cover -test-e2e: run image-registry e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster - -.PHONY: extension-developer-e2e -extension-developer-e2e: KUSTOMIZE_BUILD_DIR := config/overlays/cert-manager -extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e -extension-developer-e2e: export INSTALL_DEFAULT_CATALOGS := false -extension-developer-e2e: run image-registry test-ext-dev-e2e kind-clean #EXHELP Run extension-developer e2e on local kind cluster +test-e2e: COVERAGE_NAME := e2e +test-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-e2e: run-internal image-registry prometheus e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster + +.PHONY: test-experimental-e2e +test-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) +test-experimental-e2e: KIND_CLUSTER_NAME := operator-controller-e2e +test-experimental-e2e: GO_BUILD_EXTRA_FLAGS := -cover +test-experimental-e2e: COVERAGE_NAME := experimental-e2e +test-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-experimental-e2e: run-internal image-registry prometheus experimental-e2e e2e e2e-coverage kind-clean #HELP Run experimental e2e test suite on local kind cluster + +.PHONY: prometheus +prometheus: PROMETHEUS_NAMESPACE := olmv1-system +prometheus: PROMETHEUS_VERSION := v0.83.0 +prometheus: $(KUSTOMIZE) #EXHELP Deploy Prometheus into specified namespace + ./hack/test/install-prometheus.sh $(PROMETHEUS_NAMESPACE) $(PROMETHEUS_VERSION) $(VERSION) + +.PHONY: test-extension-developer-e2e +test-extension-developer-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) +test-extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e +test-extension-developer-e2e: export INSTALL_DEFAULT_CATALOGS := false +test-extension-developer-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-extension-developer-e2e: run-internal image-registry extension-developer-e2e kind-clean #HELP Run extension-developer e2e on local kind cluster .PHONY: run-latest-release run-latest-release: - curl -L -s https://github.com/operator-framework/operator-controller/releases/latest/download/install.sh | bash -s + curl -L -s https://github.com/operator-framework/operator-controller/releases/latest/download/$(notdir $(RELEASE_INSTALL)) | bash -s .PHONY: pre-upgrade-setup pre-upgrade-setup: @@ -270,15 +312,31 @@ pre-upgrade-setup: post-upgrade-checks: go test -count=1 -v ./test/upgrade-e2e/... + +TEST_UPGRADE_E2E_TASKS := kind-cluster run-latest-release image-registry pre-upgrade-setup docker-build kind-load kind-deploy post-upgrade-checks kind-clean + .PHONY: test-upgrade-e2e +test-upgrade-e2e: SOURCE_MANIFEST := $(STANDARD_MANIFEST) +test-upgrade-e2e: RELEASE_INSTALL := $(STANDARD_RELEASE_INSTALL) test-upgrade-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-e2e +test-upgrade-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) test-upgrade-e2e: export TEST_CLUSTER_CATALOG_NAME := test-catalog test-upgrade-e2e: export TEST_CLUSTER_EXTENSION_NAME := test-package -test-upgrade-e2e: kind-cluster run-latest-release image-registry pre-upgrade-setup docker-build kind-load kind-deploy post-upgrade-checks kind-clean #HELP Run upgrade e2e tests on a local kind cluster +test-upgrade-e2e: $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade e2e tests on a local kind cluster + +.PHONY: test-upgrade-experimental-e2e +test-upgrade-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) +test-upgrade-experimental-e2e: RELEASE_INSTALL := $(EXPERIMENTAL_RELEASE_INSTALL) +test-upgrade-experimental-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-experimental-e2e +test-upgrade-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-upgrade-experimental-e2e: export TEST_CLUSTER_CATALOG_NAME := test-catalog +test-upgrade-experimental-e2e: export TEST_CLUSTER_EXTENSION_NAME := test-package +test-upgrade-experimental-e2e: $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade e2e tests on a local kind cluster + .PHONY: e2e-coverage e2e-coverage: - COVERAGE_OUTPUT=./coverage/e2e.out ./hack/test/e2e-coverage.sh + COVERAGE_NAME=$(COVERAGE_NAME) ./hack/test/e2e-coverage.sh #SECTION KIND Cluster Operations @@ -288,22 +346,28 @@ kind-load: $(KIND) #EXHELP Loads the currently constructed images into the KIND $(CONTAINER_RUNTIME) save $(CATD_IMG) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) .PHONY: kind-deploy -kind-deploy: export MANIFEST := ./operator-controller.yaml -kind-deploy: export DEFAULT_CATALOG := ./config/catalogs/clustercatalogs/default-catalogs.yaml -kind-deploy: manifests $(KUSTOMIZE) - $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_DIR) | sed "s/cert-git-version/cert-$(VERSION)/g" > $(MANIFEST) +kind-deploy: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) +kind-deploy: manifests + @echo -e "\n\U1F4D8 Using $(SOURCE_MANIFEST) as source manifest\n" + sed "s/cert-git-version/cert-$(VERSION)/g" $(SOURCE_MANIFEST) > $(MANIFEST) + cp $(CATALOGS_MANIFEST) $(DEFAULT_CATALOG) envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh | bash -s .PHONY: kind-cluster -kind-cluster: $(KIND) #EXHELP Standup a kind cluster. +kind-cluster: $(KIND) kind-verify-versions #EXHELP Standup a kind cluster. -$(KIND) delete cluster --name $(KIND_CLUSTER_NAME) - $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --image $(KIND_CLUSTER_IMAGE) --config ./kind-config.yaml + $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config ./kind-config.yaml $(KIND) export kubeconfig --name $(KIND_CLUSTER_NAME) .PHONY: kind-clean kind-clean: $(KIND) #EXHELP Delete the kind cluster. $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) +.PHONY: kind-verify-versions +kind-verify-versions: + env K8S_VERSION=v$(K8S_VERSION) KIND=$(KIND) GOBIN=$(GOBIN) hack/tools/validate_kindest_node.sh + + #SECTION Build # attempt to generate the VERSION attribute for certificates @@ -352,8 +416,18 @@ go-build-linux: export GOOS=linux go-build-linux: export GOARCH=amd64 go-build-linux: $(BINARIES) +.PHONY: run-internal +run-internal: docker-build kind-cluster kind-load kind-deploy wait + .PHONY: run -run: docker-build kind-cluster kind-load kind-deploy wait #HELP Build the operator-controller then deploy it into a new kind cluster. +run: SOURCE_MANIFEST := $(STANDARD_MANIFEST) +run: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +run: run-internal #HELP Build operator-controller then deploy it with the standard manifest into a new kind cluster. + +.PHONY: run-experimental +run-experimental: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) +run-experimental: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +run-experimental: run-internal #HELP Build the operator-controller then deploy it with the experimental manifest into a new kind cluster. CATD_NAMESPACE := olmv1-system wait: @@ -381,16 +455,21 @@ release: $(GORELEASER) #EXHELP Runs goreleaser for the operator-controller. By d OPCON_IMAGE_REPO=$(OPCON_IMAGE_REPO) CATD_IMAGE_REPO=$(CATD_IMAGE_REPO) $(GORELEASER) $(GORELEASER_ARGS) .PHONY: quickstart -quickstart: export MANIFEST := https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/operator-controller.yaml -quickstart: export DEFAULT_CATALOG := "https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/default-catalogs.yaml" -quickstart: $(KUSTOMIZE) manifests #EXHELP Generate the unified installation release manifests and scripts. - $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_DIR) | sed "s/cert-git-version/cert-$(VERSION)/g" | sed "s/:devel/:$(VERSION)/g" > operator-controller.yaml - envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh > install.sh +quickstart: export STANDARD_MANIFEST_URL := "https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/$(notdir%20$(STANDARD_RELEASE_MANIFEST))" +quickstart: export EXPERIMENTAL_MANIFEST_URL := "https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/$(notdir%20$(EXPERIMENTAL_RELEASE_MANIFEST))" +quickstart: export DEFAULT_CATALOG := "https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/$(notdir%20$(RELEASE_CATALOGS))" +quickstart: manifests #EXHELP Generate the unified installation release manifests and scripts. + # Update the stored standard manifests for distribution + sed "s/:devel/:$(VERSION)/g" $(STANDARD_MANIFEST) | sed "s/cert-git-version/cert-$(VERSION)/g" > $(STANDARD_RELEASE_MANIFEST) + sed "s/:devel/:$(VERSION)/g" $(EXPERIMENTAL_MANIFEST) | sed "s/cert-git-version/cert-$(VERSION)/g" > $(EXPERIMENTAL_RELEASE_MANIFEST) + cp $(CATALOGS_MANIFEST) $(RELEASE_CATALOGS) + MANIFEST=$(STANDARD_MANIFEST_URL) envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh > $(STANDARD_RELEASE_INSTALL) + MANIFEST=$(EXPERIMENTAL_MANIFEST_URL) envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh > $(EXPERIMENTAL_RELEASE_INSTALL) ##@ Docs .PHONY: crd-ref-docs -API_REFERENCE_FILENAME := operator-controller-api-reference.md +API_REFERENCE_FILENAME := olmv1-api-reference.md API_REFERENCE_DIR := $(ROOT_DIR)/docs/api-reference crd-ref-docs: $(CRD_REF_DOCS) #EXHELP Generate the API Reference Documents. rm -f $(API_REFERENCE_DIR)/$(API_REFERENCE_FILENAME) @@ -403,7 +482,7 @@ VENVDIR := $(abspath docs/.venv) .PHONY: build-docs build-docs: venv . $(VENV)/activate; \ - mkdocs build + mkdocs build --strict .PHONY: serve-docs serve-docs: venv @@ -413,11 +492,15 @@ serve-docs: venv .PHONY: deploy-docs deploy-docs: venv . $(VENV)/activate; \ - mkdocs gh-deploy --force + mkdocs gh-deploy --force --strict # The demo script requires to install asciinema with: brew install asciinema to run on mac os envs. -.PHONY: demo-update #EXHELP build demo -demo-update: - ./hack/demo/generate-asciidemo.sh -u -n catalogd-demo catalogd-demo-script.sh +# Please ensure that all demos are named with the demo name and the suffix -demo-script.sh +.PHONY: update-demos #EXHELP Update and upload the demos. +update-demos: + @for script in hack/demo/*-demo-script.sh; do \ + nm=$$(basename $$script -script.sh); \ + ./hack/demo/generate-asciidemo.sh -u -n $$nm $$(basename $$script); \ + done include Makefile.venv diff --git a/OWNERS b/OWNERS index 6dbe776904..b7e9325fea 100644 --- a/OWNERS +++ b/OWNERS @@ -1,5 +1,4 @@ approvers: - - operator-controller-approvers + - olmv1-approvers reviewers: - - operator-controller-approvers - - operator-controller-reviewers + - olmv1-reviewers diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES index 52dba52e08..1776b9b654 100644 --- a/OWNERS_ALIASES +++ b/OWNERS_ALIASES @@ -1,25 +1,50 @@ - aliases: - # contributors who can approve any PRs in the repo - operator-controller-approvers: - - camilamacedo86 - - grokspawn + olmv1-approvers: - joelanford - kevinrizza - perdasilva - - thetechnick - tmshort - # contributors who can review/lgtm any PRs in the repo - operator-controller-reviewers: + olmv1-reviewers: - anik120 - ankitathomas - bentito + - camilamacedo86 - dtfranz - - gallettilance - - gavinmbell - - LalatenduMohanty + - grokspawn + - joelanford - oceanc80 - OchiengEd + - perdasilva - rashmigottipati + - thetechnick + - tmshort - trgeiger + - pedjak + + api-approvers: + - grokspawn + - thetechnick + + catalogd-approvers: + - grokspawn + + operator-controller-approvers: + - thetechnick + + cmd-approvers: + - grokspawn + + manifest-approvers: + - camilamacedo86 + + ci-approvers: + - camilamacedo86 + + docs-approvers: + - michaelryanpeter + + docs-draft-approvers: + - camilamacedo86 + - grokspawn + - thetechnick diff --git a/README.md b/README.md index 783276d9b0..4be7f30d08 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![unit-test](https://github.com/operator-framework/operator-controller/actions/workflows/unit-test.yaml/badge.svg)](https://github.com/operator-framework/operator-controller/actions/workflows/unit-test.yaml) +[![e2e](https://github.com/operator-framework/operator-controller/actions/workflows/e2e.yaml/badge.svg)](https://github.com/operator-framework/operator-controller/actions/workflows/e2e.yaml) +[![codecov](https://codecov.io/gh/operator-framework/operator-controller/graph/badge.svg?token=5f34zaWaN7)](https://codecov.io/gh/operator-framework/operator-controller) + # operator-controller The operator-controller is the central component of Operator Lifecycle Manager (OLM) v1. It extends Kubernetes with an API through which users can install extensions. diff --git a/Tiltfile b/Tiltfile index 2d5e363816..d736b8f94d 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,24 +1,23 @@ load('.tilt-support', 'deploy_repo') -operator_controller = { - 'image': 'quay.io/operator-framework/operator-controller', - 'yaml': 'config/overlays/tilt-local-dev/operator-controller', - 'binaries': { - './cmd/operator-controller': 'operator-controller-controller-manager', +olmv1 = { + 'repos': { + 'catalogd': { + 'image': 'quay.io/operator-framework/catalogd', + 'binary': './cmd/catalogd', + 'deployment': 'catalogd-controller-manager', + 'deps': ['api', 'cmd/catalogd', 'internal/catalogd', 'internal/shared', 'go.mod', 'go.sum'], + 'starting_debug_port': 20000, + }, + 'operator-controller': { + 'image': 'quay.io/operator-framework/operator-controller', + 'binary': './cmd/operator-controller', + 'deployment': 'operator-controller-controller-manager', + 'deps': ['api', 'cmd/operator-controller', 'internal/operator-controller', 'internal/shared', 'go.mod', 'go.sum'], + 'starting_debug_port': 30000, + }, }, - 'deps': ['api', 'cmd/operator-controller', 'internal/operator-controller', 'internal/shared', 'go.mod', 'go.sum'], - 'starting_debug_port': 30000, + 'yaml': 'helm/tilt.yaml', } -deploy_repo('operator-controller', operator_controller, '-tags containers_image_openpgp') -catalogd = { - 'image': 'quay.io/operator-framework/catalogd', - 'yaml': 'config/overlays/tilt-local-dev/catalogd', - 'binaries': { - './cmd/catalogd': 'catalogd-controller-manager', - }, - 'deps': ['api', 'cmd/catalogd', 'internal/catalogd', 'internal/shared', 'go.mod', 'go.sum'], - 'starting_debug_port': 20000, -} - -deploy_repo('catalogd', catalogd, '-tags containers_image_openpgp') +deploy_repo(olmv1, '-tags containers_image_openpgp') diff --git a/api/OWNERS b/api/OWNERS new file mode 100644 index 0000000000..71df7cfc52 --- /dev/null +++ b/api/OWNERS @@ -0,0 +1,2 @@ +approvers: + - api-approvers diff --git a/api/v1/clustercatalog_types.go b/api/v1/clustercatalog_types.go index ee1391b79c..c18fa3c7e6 100644 --- a/api/v1/clustercatalog_types.go +++ b/api/v1/clustercatalog_types.go @@ -182,7 +182,7 @@ type ClusterCatalogStatus struct { // +listType=map // +listMapKey=type // +optional - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + Conditions []metav1.Condition `json:"conditions,omitempty"` // resolvedSource contains information about the resolved source based on the source type. // +optional ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"` diff --git a/api/v1/clustercatalog_types_test.go b/api/v1/clustercatalog_types_test.go index 4f86fd0fed..71a64bc9e9 100644 --- a/api/v1/clustercatalog_types_test.go +++ b/api/v1/clustercatalog_types_test.go @@ -20,7 +20,7 @@ import ( "sigs.k8s.io/yaml" ) -const crdFilePath = "../../config/base/catalogd/crd/bases/olm.operatorframework.io_clustercatalogs.yaml" +const crdFilePath = "../../helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml" func TestImageSourceCELValidationRules(t *testing.T) { validators := fieldValidatorsFromFile(t, crdFilePath) @@ -149,7 +149,7 @@ func TestImageSourceCELValidationRules(t *testing.T) { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.spec) //nolint:gosec require.NoError(t, err) errs := validator(obj, nil) - require.Equal(t, len(tc.wantErrs), len(errs), "want", tc.wantErrs, "got", errs) + require.Len(t, errs, len(tc.wantErrs), "want", tc.wantErrs, "got", errs) for i := range tc.wantErrs { got := errs[i].Error() assert.Equal(t, tc.wantErrs[i], got) @@ -242,7 +242,7 @@ func TestResolvedImageSourceCELValidation(t *testing.T) { } { t.Run(name, func(t *testing.T) { errs := validator(tc.spec.Ref, nil) - require.Equal(t, len(tc.wantErrs), len(errs), "want", tc.wantErrs, "got", errs) + require.Len(t, errs, len(tc.wantErrs), "want", tc.wantErrs, "got", errs) for i := range tc.wantErrs { got := errs[i].Error() assert.Equal(t, tc.wantErrs[i], got) @@ -286,7 +286,7 @@ func TestClusterCatalogURLsCELValidation(t *testing.T) { t.Run(name, func(t *testing.T) { errs := validator(tc.urls.Base, nil) fmt.Println(errs) - require.Equal(t, len(tc.wantErrs), len(errs)) + require.Len(t, errs, len(tc.wantErrs)) for i := range tc.wantErrs { got := errs[i].Error() assert.Equal(t, tc.wantErrs[i], got) @@ -327,7 +327,7 @@ func TestSourceCELValidation(t *testing.T) { require.NoError(t, err) errs := validator(obj, nil) fmt.Println(errs) - require.Equal(t, len(tc.wantErrs), len(errs)) + require.Len(t, errs, len(tc.wantErrs)) for i := range tc.wantErrs { got := errs[i].Error() assert.Equal(t, tc.wantErrs[i], got) @@ -368,7 +368,7 @@ func TestResolvedSourceCELValidation(t *testing.T) { obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.source) //nolint:gosec require.NoError(t, err) errs := validator(obj, nil) - require.Equal(t, len(tc.wantErrs), len(errs)) + require.Len(t, errs, len(tc.wantErrs)) for i := range tc.wantErrs { got := errs[i].Error() assert.Equal(t, tc.wantErrs[i], got) diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 0141f1a7a4..343e8cbb4a 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -25,6 +26,8 @@ var ClusterExtensionKind = "ClusterExtension" type ( UpgradeConstraintPolicy string CRDUpgradeSafetyEnforcement string + + ClusterExtensionConfigType string ) const ( @@ -39,6 +42,8 @@ const ( // Use with caution as this can lead to unknown and potentially // disastrous results such as data loss. UpgradeConstraintPolicySelfCertified UpgradeConstraintPolicy = "SelfCertified" + + ClusterExtensionConfigTypeInline ClusterExtensionConfigType = "Inline" ) // ClusterExtensionSpec defines the desired state of ClusterExtension @@ -92,6 +97,17 @@ type ClusterExtensionSpec struct { // // +optional Install *ClusterExtensionInstallConfig `json:"install,omitempty"` + + // config is an optional field used to specify bundle specific configuration + // used to configure the bundle. Configuration is bundle specific and a bundle may provide + // a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + // + // config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + // a configuration schema the final manifests will be derived on a best-effort basis. More information on how + // to configure the bundle should be found in its end-user documentation. + // + // +optional + Config *ClusterExtensionConfig `json:"config,omitempty"` } const SourceTypeCatalog = "Catalog" @@ -138,6 +154,36 @@ type ClusterExtensionInstallConfig struct { Preflight *PreflightConfig `json:"preflight,omitempty"` } +// ClusterExtensionConfig is a discriminated union which selects the source configuration values to be merged into +// the ClusterExtension's rendered manifests. +// +// +kubebuilder:validation:XValidation:rule="has(self.configType) && self.configType == 'Inline' ?has(self.inline) : !has(self.inline)",message="inline is required when configType is Inline, and forbidden otherwise" +// +union +type ClusterExtensionConfig struct { + // configType is a required reference to the type of configuration source. + // + // Allowed values are "Inline" + // + // When this field is set to "Inline", the cluster extension configuration is defined inline within the + // ClusterExtension resource. + // + // +unionDiscriminator + // +kubebuilder:validation:Enum:="Inline" + // +kubebuilder:validation:Required + ConfigType ClusterExtensionConfigType `json:"configType"` + + // inline contains JSON or YAML values specified directly in the + // ClusterExtension. + // + // inline must be set if configType is 'Inline'. + // inline accepts arbitrary JSON/YAML objects. + // inline is validation at runtime against the schema provided by the bundle if a schema is provided. + // + // +kubebuilder:validation:Type=object + // +optional + Inline *apiextensionsv1.JSON `json:"inline,omitempty"` +} + // CatalogFilter defines the attributes used to identify and filter content from a catalog. type CatalogFilter struct { // packageName is a reference to the name of the package to be installed @@ -443,12 +489,10 @@ type ClusterExtensionStatus struct { // PackageDeprecated is set if the requested package is marked deprecated in the catalog. // Deprecated is a rollup condition that is present when any of the deprecated conditions are present. // - // +patchMergeKey=type - // +patchStrategy=merge // +listType=map // +listMapKey=type // +optional - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + Conditions []metav1.Condition `json:"conditions,omitempty"` // install is a representation of the current installation status for this ClusterExtension. // diff --git a/api/v1/clusterextension_types_test.go b/api/v1/clusterextension_types_test.go index 7bc9a33934..feb9b4e224 100644 --- a/api/v1/clusterextension_types_test.go +++ b/api/v1/clusterextension_types_test.go @@ -14,6 +14,8 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" ) +// TODO Expand these tests to cover Types/Reasons/etc. from other APIs as well + func TestClusterExtensionTypeRegistration(t *testing.T) { types, err := parseConstants("Type") if err != nil { diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go new file mode 100644 index 0000000000..13ac4ce2a7 --- /dev/null +++ b/api/v1/clusterextensionrevision_types.go @@ -0,0 +1,184 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +const ( + ClusterExtensionRevisionKind = "ClusterExtensionRevision" + + // Condition Types + ClusterExtensionRevisionTypeAvailable = "Available" + ClusterExtensionRevisionTypeSucceeded = "Succeeded" + + // Condition Reasons + ClusterExtensionRevisionReasonAvailable = "Available" + ClusterExtensionRevisionReasonReconcileFailure = "ReconcileFailure" + ClusterExtensionRevisionReasonRevisionValidationFailure = "RevisionValidationFailure" + ClusterExtensionRevisionReasonPhaseValidationError = "PhaseValidationError" + ClusterExtensionRevisionReasonObjectCollisions = "ObjectCollisions" + ClusterExtensionRevisionReasonRolloutSuccess = "RolloutSuccess" + ClusterExtensionRevisionReasonProbeFailure = "ProbeFailure" + ClusterExtensionRevisionReasonIncomplete = "Incomplete" + ClusterExtensionRevisionReasonProgressing = "Progressing" + ClusterExtensionRevisionReasonArchived = "Archived" +) + +// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision. +type ClusterExtensionRevisionSpec struct { + // Specifies the lifecycle state of the ClusterExtensionRevision. + // + // +kubebuilder:default="Active" + // +kubebuilder:validation:Enum=Active;Paused;Archived + // +kubebuilder:validation:XValidation:rule="oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' && oldSelf == self", message="can not un-archive" + LifecycleState ClusterExtensionRevisionLifecycleState `json:"lifecycleState,omitempty"` + // Revision is a sequence number representing a specific revision of the ClusterExtension instance. + // Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + // a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" + Revision int64 `json:"revision"` + // Phases are groups of objects that will be applied at the same time. + // All objects in the phase will have to pass their probes in order to progress to the next phase. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf || oldSelf.size() == 0", message="phases is immutable" + // +listType=map + // +listMapKey=name + // +optional + Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` + // Previous references previous revisions that objects can be adopted from. + // + // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="previous is immutable" + Previous []ClusterExtensionRevisionPrevious `json:"previous,omitempty"` +} + +// ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. +type ClusterExtensionRevisionLifecycleState string + +const ( + // ClusterExtensionRevisionLifecycleStateActive / "Active" is the default lifecycle state. + ClusterExtensionRevisionLifecycleStateActive ClusterExtensionRevisionLifecycleState = "Active" + // ClusterExtensionRevisionLifecycleStatePaused / "Paused" disables reconciliation of the ClusterExtensionRevision. + // Only Status updates will still propagated, but object changes will not be reconciled. + ClusterExtensionRevisionLifecycleStatePaused ClusterExtensionRevisionLifecycleState = "Paused" + // ClusterExtensionRevisionLifecycleStateArchived / "Archived" disables reconciliation while also "scaling to zero", + // which deletes all objects that are not excluded via the pausedFor property and + // removes itself from the owner list of all other objects previously under management. + ClusterExtensionRevisionLifecycleStateArchived ClusterExtensionRevisionLifecycleState = "Archived" +) + +// ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. +// All objects in the a phase will have to pass their probes in order to progress to the next phase. +type ClusterExtensionRevisionPhase struct { + // Name identifies this phase. + // + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$` + Name string `json:"name"` + // Objects are a list of all the objects within this phase. + Objects []ClusterExtensionRevisionObject `json:"objects"` +} + +// ClusterExtensionRevisionObject contains an object and settings for it. +type ClusterExtensionRevisionObject struct { + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:pruning:PreserveUnknownFields + Object unstructured.Unstructured `json:"object"` + // CollisionProtection controls whether OLM can adopt and modify objects + // already existing on the cluster or even owned by another controller. + // + // +kubebuilder:default="Prevent" + // +optional + CollisionProtection CollisionProtection `json:"collisionProtection,omitempty"` +} + +// CollisionProtection specifies if and how ownership collisions are prevented. +type CollisionProtection string + +const ( + // CollisionProtectionPrevent prevents owner collisions entirely + // by only allowing to work with objects itself has created. + CollisionProtectionPrevent CollisionProtection = "Prevent" + // CollisionProtectionIfNoController allows to patch and override + // objects already present if they are not owned by another controller. + CollisionProtectionIfNoController CollisionProtection = "IfNoController" + // CollisionProtectionNone allows to patch and override objects + // already present and owned by other controllers. + // Be careful! This setting may cause multiple controllers to fight over a resource, + // causing load on the API server and etcd. + CollisionProtectionNone CollisionProtection = "None" +) + +type ClusterExtensionRevisionPrevious struct { + // +kubebuilder:validation:Required + Name string `json:"name"` + // +kubebuilder:validation:Required + UID types.UID `json:"uid"` +} + +// ClusterExtensionRevisionStatus defines the observed state of a ClusterExtensionRevision. +type ClusterExtensionRevisionStatus struct { + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status + +// ClusterExtensionRevision is the Schema for the clusterextensionrevisions API +// +kubebuilder:printcolumn:name="Available",type=string,JSONPath=`.status.conditions[?(@.type=='Available')].status` +// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` +type ClusterExtensionRevision struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is an optional field that defines the desired state of the ClusterExtension. + // +optional + Spec ClusterExtensionRevisionSpec `json:"spec,omitempty"` + + // status is an optional field that defines the observed state of the ClusterExtension. + // +optional + Status ClusterExtensionRevisionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterExtensionRevisionList contains a list of ClusterExtensionRevision +type ClusterExtensionRevisionList struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items is a required list of ClusterExtensionRevision objects. + // + // +kubebuilder:validation:Required + Items []ClusterExtensionRevision `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) +} diff --git a/api/v1/clusterextensionrevision_types_test.go b/api/v1/clusterextensionrevision_types_test.go new file mode 100644 index 0000000000..9792826fb8 --- /dev/null +++ b/api/v1/clusterextensionrevision_types_test.go @@ -0,0 +1,142 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestClusterExtensionRevisionImmutability(t *testing.T) { + c := newClient(t) + ctx := context.Background() + i := 0 + for name, tc := range map[string]struct { + spec ClusterExtensionRevisionSpec + updateFunc func(*ClusterExtensionRevision) + allowed bool + }{ + "revision is immutable": { + spec: ClusterExtensionRevisionSpec{ + Revision: 1, + }, + updateFunc: func(cer *ClusterExtensionRevision) { + cer.Spec.Revision = 2 + }, + }, + "phases may be initially empty": { + spec: ClusterExtensionRevisionSpec{ + Revision: 1, + Phases: []ClusterExtensionRevisionPhase{}, + }, + updateFunc: func(cer *ClusterExtensionRevision) { + cer.Spec.Phases = []ClusterExtensionRevisionPhase{ + { + Name: "foo", + Objects: []ClusterExtensionRevisionObject{}, + }, + } + }, + allowed: true, + }, + "phases may be initially unset": { + spec: ClusterExtensionRevisionSpec{ + Revision: 1, + }, + updateFunc: func(cer *ClusterExtensionRevision) { + cer.Spec.Phases = []ClusterExtensionRevisionPhase{ + { + Name: "foo", + Objects: []ClusterExtensionRevisionObject{}, + }, + } + }, + allowed: true, + }, + "phases are immutable if not empty": { + spec: ClusterExtensionRevisionSpec{ + Revision: 1, + Phases: []ClusterExtensionRevisionPhase{ + { + Name: "foo", + Objects: []ClusterExtensionRevisionObject{}, + }, + }, + }, + updateFunc: func(cer *ClusterExtensionRevision) { + cer.Spec.Phases = []ClusterExtensionRevisionPhase{ + { + Name: "foo2", + Objects: []ClusterExtensionRevisionObject{}, + }, + } + }, + }, + } { + t.Run(name, func(t *testing.T) { + cer := &ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo%d", i), + }, + Spec: tc.spec, + } + i = i + 1 + require.NoError(t, c.Create(ctx, cer)) + tc.updateFunc(cer) + err := c.Update(ctx, cer) + if tc.allowed && err != nil { + t.Fatal("expected update to succeed, but got:", err) + } + if !tc.allowed && !errors.IsInvalid(err) { + t.Fatal("expected update to fail due to invalid payload, but got:", err) + } + }) + } +} + +func TestClusterExtensionRevisionValidity(t *testing.T) { + c := newClient(t) + ctx := context.Background() + i := 0 + for name, tc := range map[string]struct { + spec ClusterExtensionRevisionSpec + valid bool + }{ + "revision cannot be negative": { + spec: ClusterExtensionRevisionSpec{ + Revision: -1, + }, + valid: false, + }, + "revision cannot be zero": { + spec: ClusterExtensionRevisionSpec{}, + valid: false, + }, + "revision must be positive": { + spec: ClusterExtensionRevisionSpec{ + Revision: 1, + }, + valid: true, + }, + } { + t.Run(name, func(t *testing.T) { + cer := &ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("bar%d", i), + }, + Spec: tc.spec, + } + i = i + 1 + err := c.Create(ctx, cer) + if tc.valid && err != nil { + t.Fatal("expected create to succeed, but got:", err) + } + if !tc.valid && !errors.IsInvalid(err) { + t.Fatal("expected create to fail due to invalid payload, but got:", err) + } + }) + } +} diff --git a/api/v1/common_types.go b/api/v1/common_types.go index 5478039c96..6ab5336ac2 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -20,12 +20,18 @@ const ( TypeInstalled = "Installed" TypeProgressing = "Progressing" + // Installed reasons + ReasonAbsent = "Absent" + // Progressing reasons - ReasonSucceeded = "Succeeded" - ReasonRetrying = "Retrying" - ReasonBlocked = "Blocked" + ReasonRolloutInProgress = "RolloutInProgress" + ReasonRetrying = "Retrying" + ReasonBlocked = "Blocked" - // Terminal reasons + // Deprecation reasons ReasonDeprecated = "Deprecated" - ReasonFailed = "Failed" + + // Common reasons + ReasonSucceeded = "Succeeded" + ReasonFailed = "Failed" ) diff --git a/api/v1/suite_test.go b/api/v1/suite_test.go new file mode 100644 index 0000000000..bc7a0c22bc --- /dev/null +++ b/api/v1/suite_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "log" + "os" + "testing" + + "github.com/stretchr/testify/require" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/test" +) + +func newScheme(t *testing.T) *apimachineryruntime.Scheme { + sch := apimachineryruntime.NewScheme() + require.NoError(t, AddToScheme(sch)) + return sch +} + +func newClient(t *testing.T) client.Client { + cl, err := client.New(config, client.Options{Scheme: newScheme(t)}) + require.NoError(t, err) + require.NotNil(t, cl) + return cl +} + +var config *rest.Config + +func TestMain(m *testing.M) { + testEnv := test.NewEnv() + + var err error + config, err = testEnv.Start() + utilruntime.Must(err) + if config == nil { + log.Panic("expected cfg to not be nil") + } + + code := m.Run() + utilruntime.Must(testEnv.Stop()) + os.Exit(code) +} diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 37694f61f3..e13f1532b0 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -65,8 +65,7 @@ func (in *CatalogFilter) DeepCopyInto(out *CatalogFilter) { } if in.Selector != nil { in, out := &in.Selector, &out.Selector - *out = new(metav1.LabelSelector) - (*in).DeepCopyInto(*out) + *out = (*in).DeepCopy() } } @@ -253,6 +252,25 @@ func (in *ClusterExtension) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionConfig) DeepCopyInto(out *ClusterExtensionConfig) { + *out = *in + if in.Inline != nil { + in, out := &in.Inline, &out.Inline + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionConfig. +func (in *ClusterExtensionConfig) DeepCopy() *ClusterExtensionConfig { + if in == nil { + return nil + } + out := new(ClusterExtensionConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtensionInstallConfig) DeepCopyInto(out *ClusterExtensionInstallConfig) { *out = *in @@ -321,6 +339,167 @@ func (in *ClusterExtensionList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevision) DeepCopyInto(out *ClusterExtensionRevision) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevision. +func (in *ClusterExtensionRevision) DeepCopy() *ClusterExtensionRevision { + if in == nil { + return nil + } + out := new(ClusterExtensionRevision) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExtensionRevision) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionList) DeepCopyInto(out *ClusterExtensionRevisionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterExtensionRevision, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionList. +func (in *ClusterExtensionRevisionList) DeepCopy() *ClusterExtensionRevisionList { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterExtensionRevisionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionObject) DeepCopyInto(out *ClusterExtensionRevisionObject) { + *out = *in + in.Object.DeepCopyInto(&out.Object) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionObject. +func (in *ClusterExtensionRevisionObject) DeepCopy() *ClusterExtensionRevisionObject { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionObject) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionPhase) DeepCopyInto(out *ClusterExtensionRevisionPhase) { + *out = *in + if in.Objects != nil { + in, out := &in.Objects, &out.Objects + *out = make([]ClusterExtensionRevisionObject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionPhase. +func (in *ClusterExtensionRevisionPhase) DeepCopy() *ClusterExtensionRevisionPhase { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionPhase) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionPrevious) DeepCopyInto(out *ClusterExtensionRevisionPrevious) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionPrevious. +func (in *ClusterExtensionRevisionPrevious) DeepCopy() *ClusterExtensionRevisionPrevious { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionPrevious) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionSpec) DeepCopyInto(out *ClusterExtensionRevisionSpec) { + *out = *in + if in.Phases != nil { + in, out := &in.Phases, &out.Phases + *out = make([]ClusterExtensionRevisionPhase, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Previous != nil { + in, out := &in.Previous, &out.Previous + *out = make([]ClusterExtensionRevisionPrevious, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionSpec. +func (in *ClusterExtensionRevisionSpec) DeepCopy() *ClusterExtensionRevisionSpec { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterExtensionRevisionStatus) DeepCopyInto(out *ClusterExtensionRevisionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionRevisionStatus. +func (in *ClusterExtensionRevisionStatus) DeepCopy() *ClusterExtensionRevisionStatus { + if in == nil { + return nil + } + out := new(ClusterExtensionRevisionStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = *in @@ -331,6 +510,11 @@ func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = new(ClusterExtensionInstallConfig) (*in).DeepCopyInto(*out) } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(ClusterExtensionConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionSpec. diff --git a/cmd/OWNERS b/cmd/OWNERS new file mode 100644 index 0000000000..740420d64f --- /dev/null +++ b/cmd/OWNERS @@ -0,0 +1,2 @@ +approvers: + - cmd-approvers diff --git a/cmd/catalogd/main.go b/cmd/catalogd/main.go index 9499a7006f..36f7b16752 100644 --- a/cmd/catalogd/main.go +++ b/cmd/catalogd/main.go @@ -28,11 +28,8 @@ import ( "strings" "time" - "github.com/containers/image/v5/types" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - k8slabels "k8s.io/apimachinery/pkg/labels" + "go.podman.io/image/v5/types" "k8s.io/apimachinery/pkg/runtime" k8stypes "k8s.io/apimachinery/pkg/types" apimachineryrand "k8s.io/apimachinery/pkg/util/rand" @@ -61,8 +58,13 @@ import ( "github.com/operator-framework/operator-controller/internal/catalogd/serverutil" "github.com/operator-framework/operator-controller/internal/catalogd/storage" "github.com/operator-framework/operator-controller/internal/catalogd/webhook" + sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" + httputil "github.com/operator-framework/operator-controller/internal/shared/util/http" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" + "github.com/operator-framework/operator-controller/internal/shared/util/pullsecretcache" + sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa" + "github.com/operator-framework/operator-controller/internal/shared/util/tlsprofiles" "github.com/operator-framework/operator-controller/internal/shared/version" ) @@ -141,6 +143,7 @@ func init() { klog.InitFlags(flag.CommandLine) flags.AddGoFlagSet(flag.CommandLine) features.CatalogdFeatureGate.AddFlag(flags) + tlsprofiles.AddFlags(flags) utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(ocv1.AddToScheme(scheme)) @@ -215,12 +218,18 @@ func run(ctx context.Context) error { // For details, see: https://github.com/kubernetes/kubernetes/issues/121197 config.NextProtos = []string{"http/1.1"} } + tlsProfile, err := tlsprofiles.GetTLSConfigFunc() + if err != nil { + setupLog.Error(err, "failed to get TLS profile") + return err + } // Create webhook server and configure TLS webhookServer := crwebhook.NewServer(crwebhook.Options{ Port: cfg.webhookPort, TLSOpts: []func(*tls.Config){ tlsOpts, + tlsProfile, }, }) @@ -232,7 +241,7 @@ func run(ctx context.Context) error { metricsServerOptions.SecureServing = true metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization - metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, tlsOpts) + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, tlsOpts, tlsProfile) } else { // Note that the metrics server is not serving if the BindAddress is set to "0". // Therefore, the metrics server is disabled by default. It is only enabled @@ -246,17 +255,19 @@ func run(ctx context.Context) error { cacheOptions := crcache.Options{ ByObject: map[client.Object]crcache.ByObject{}, } - if cfg.globalPullSecretKey != nil { - cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{ - Namespaces: map[string]crcache.Config{ - cfg.globalPullSecretKey.Namespace: { - LabelSelector: k8slabels.Everything(), - FieldSelector: fields.SelectorFromSet(map[string]string{ - "metadata.name": cfg.globalPullSecretKey.Name, - }), - }, - }, - } + + saKey, err := sautil.GetServiceAccount() + if err != nil { + setupLog.Error(err, "Failed to extract serviceaccount from JWT") + return err + } + setupLog.Info("Successfully extracted serviceaccount from JWT", "serviceaccount", + fmt.Sprintf("%s/%s", saKey.Namespace, saKey.Name)) + + err = pullsecretcache.SetupPullSecretCache(&cacheOptions, cfg.globalPullSecretKey, saKey) + if err != nil { + setupLog.Error(err, "Unable to setup pull-secret cache") + return err } // Create manager @@ -289,6 +300,18 @@ func run(ctx context.Context) error { return err } + // This watches the pullCasDir and the SSL_CERT_DIR, and SSL_CERT_FILE for changes + cpwPull, err := httputil.NewCertPoolWatcher(cfg.pullCasDir, ctrl.Log.WithName("pull-ca-pool")) + if err != nil { + setupLog.Error(err, "unable to create pull-ca-pool watcher") + return err + } + cpwPull.Restart(os.Exit) + if err = mgr.Add(cpwPull); err != nil { + setupLog.Error(err, "unable to add pull-ca-pool watcher to manager") + return err + } + if cfg.systemNamespace == "" { cfg.systemNamespace = podNamespace() } @@ -312,7 +335,7 @@ func run(ctx context.Context) error { DockerCertPath: cfg.pullCasDir, OCICertPath: cfg.pullCasDir, } - if _, err := os.Stat(authFilePath); err == nil && cfg.globalPullSecretKey != nil { + if _, err := os.Stat(authFilePath); err == nil { logger.Info("using available authentication information for pulling image") srcContext.AuthFilePath = authFilePath } else if os.IsNotExist(err) { @@ -370,17 +393,16 @@ func run(ctx context.Context) error { return err } - if cfg.globalPullSecretKey != nil { - setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) - err := (&corecontrollers.PullSecretReconciler{ - Client: mgr.GetClient(), - AuthFilePath: authFilePath, - SecretKey: *cfg.globalPullSecretKey, - }).SetupWithManager(mgr) - if err != nil { - setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") - return err - } + setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) + err = (&sharedcontrollers.PullSecretReconciler{ + Client: mgr.GetClient(), + AuthFilePath: authFilePath, + SecretKey: cfg.globalPullSecretKey, + ServiceAccountKey: saKey, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") + return err } //+kubebuilder:scaffold:builder diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 6125472484..fba7c39af5 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -28,26 +28,32 @@ import ( "strings" "time" - "github.com/containers/image/v5/types" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" + "go.podman.io/image/v5/types" rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" - "k8s.io/apimachinery/pkg/fields" k8slabels "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" apimachineryrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/discovery" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/klog/v2" "k8s.io/utils/ptr" + "pkg.package-operator.run/boxcutter/machinery" + "pkg.package-operator.run/boxcutter/managedcache" + "pkg.package-operator.run/boxcutter/ownerhandling" + "pkg.package-operator.run/boxcutter/validation" ctrl "sigs.k8s.io/controller-runtime" crcache "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/client" + crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -65,15 +71,18 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" + sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" httputil "github.com/operator-framework/operator-controller/internal/shared/util/http" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" + "github.com/operator-framework/operator-controller/internal/shared/util/pullsecretcache" + sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa" + "github.com/operator-framework/operator-controller/internal/shared/util/tlsprofiles" "github.com/operator-framework/operator-controller/internal/shared/version" ) @@ -98,7 +107,10 @@ type config struct { globalPullSecret string } -const authFilePrefix = "operator-controller-global-pull-secrets" +const ( + authFilePrefix = "operator-controller-global-pull-secrets" + fieldOwnerPrefix = "olm.operatorframework.io" +) // podNamespace checks whether the controller is running in a Pod vs. // being run locally by inspecting the namespace file that gets mounted @@ -158,6 +170,9 @@ func init() { //add feature gate flags to flagset features.OperatorControllerFeatureGate.AddFlag(flags) + //add TLS flags + tlsprofiles.AddFlags(flags) + ctrl.SetLogger(klog.NewKlogr()) } func validateMetricsFlags() error { @@ -217,19 +232,27 @@ func run() error { }, DefaultLabelSelector: k8slabels.Nothing(), } - if globalPullSecretKey != nil { - cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{ - Namespaces: map[string]crcache.Config{ - globalPullSecretKey.Namespace: { - LabelSelector: k8slabels.Everything(), - FieldSelector: fields.SelectorFromSet(map[string]string{ - "metadata.name": globalPullSecretKey.Name, - }), - }, - }, + + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + cacheOptions.ByObject[&ocv1.ClusterExtensionRevision{}] = crcache.ByObject{ + Label: k8slabels.Everything(), } } + saKey, err := sautil.GetServiceAccount() + if err != nil { + setupLog.Error(err, "Failed to extract serviceaccount from JWT") + return err + } + setupLog.Info("Successfully extracted serviceaccount from JWT", "serviceaccount", + fmt.Sprintf("%s/%s", saKey.Namespace, saKey.Name)) + + err = pullsecretcache.SetupPullSecretCache(&cacheOptions, globalPullSecretKey, saKey) + if err != nil { + setupLog.Error(err, "Unable to setup pull-secret cache") + return err + } + metricsServerOptions := server.Options{} if len(cfg.certFile) > 0 && len(cfg.keyFile) > 0 { setupLog.Info("Starting metrics server with TLS enabled", "addr", cfg.metricsAddr, "tls-cert", cfg.certFile, "tls-key", cfg.keyFile) @@ -258,6 +281,12 @@ func run() error { // the risks. More info https://github.com/golang/go/issues/63417 config.NextProtos = []string{"http/1.1"} }) + tlsProfile, err := tlsprofiles.GetTLSConfigFunc() + if err != nil { + setupLog.Error(err, "failed to get TLS profile") + return err + } + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, tlsProfile) } else { // Note that the metrics server is not serving if the BindAddress is set to "0". // Therefore, the metrics server is disabled by default. It is only enabled @@ -269,7 +298,8 @@ func run() error { "Metrics will not be served since the TLS certificate and key file are not provided.") } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + restConfig := ctrl.GetConfigOrDie() + mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ Scheme: scheme.Scheme, Metrics: metricsServerOptions, PprofBindAddress: cfg.pprofAddr, @@ -301,41 +331,26 @@ func run() error { return err } - coreClient, err := corev1client.NewForConfig(mgr.GetConfig()) + cpwCatalogd, err := httputil.NewCertPoolWatcher(cfg.catalogdCasDir, ctrl.Log.WithName("catalogd-ca-pool")) if err != nil { - setupLog.Error(err, "unable to create core client") + setupLog.Error(err, "unable to create catalogd-ca-pool watcher") return err } - tokenGetter := authentication.NewTokenGetter(coreClient, authentication.WithExpirationDuration(1*time.Hour)) - clientRestConfigMapper := action.ServiceAccountRestConfigMapper(tokenGetter) - if features.OperatorControllerFeatureGate.Enabled(features.SyntheticPermissions) { - clientRestConfigMapper = action.SyntheticUserRestConfigMapper(clientRestConfigMapper) - } - - cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), - helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)), - helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) { - ext := obj.(*ocv1.ClusterExtension) - return ext.Spec.Namespace, nil - }), - helmclient.ClientRestConfigMapper(clientRestConfigMapper), - ) - if err != nil { - setupLog.Error(err, "unable to config for creating helm client") + cpwCatalogd.Restart(os.Exit) + if err = mgr.Add(cpwCatalogd); err != nil { + setupLog.Error(err, "unable to add catalogd-ca-pool watcher to manager") return err } - acg, err := action.NewWrappedActionClientGetter(cfgGetter, - helmclient.WithFailureRollbacks(false), - ) + // This watches the pullCasDir and the SSL_CERT_DIR, and SSL_CERT_FILE for changes + cpwPull, err := httputil.NewCertPoolWatcher(cfg.pullCasDir, ctrl.Log.WithName("pull-ca-pool")) if err != nil { - setupLog.Error(err, "unable to create helm client") + setupLog.Error(err, "unable to create pull-ca-pool watcher") return err } - - certPoolWatcher, err := httputil.NewCertPoolWatcher(cfg.catalogdCasDir, ctrl.Log.WithName("cert-pool")) - if err != nil { - setupLog.Error(err, "unable to create CA certificate pool") + cpwPull.Restart(os.Exit) + if err = mgr.Add(cpwPull); err != nil { + setupLog.Error(err, "unable to add pull-ca-pool watcher to manager") return err } @@ -360,7 +375,7 @@ func run() error { OCICertPath: cfg.pullCasDir, } logger := log.FromContext(ctx) - if _, err := os.Stat(authFilePath); err == nil && globalPullSecretKey != nil { + if _, err := os.Stat(authFilePath); err == nil { logger.Info("using available authentication information for pulling image") srcContext.AuthFilePath = authFilePath } else if os.IsNotExist(err) { @@ -389,7 +404,7 @@ func run() error { } catalogClientBackend := cache.NewFilesystemCache(catalogsCachePath) catalogClient := catalogclient.New(catalogClientBackend, func() (*http.Client, error) { - return httputil.BuildHTTPClient(certPoolWatcher) + return httputil.BuildHTTPClient(cpwCatalogd) }) resolver := &resolve.CatalogResolver{ @@ -418,58 +433,39 @@ func run() error { crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()), } - // determine if PreAuthorizer should be enabled based on feature gate - var preAuth authorization.PreAuthorizer - if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { - preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) - } - - // determine if a certificate provider should be set in the bundle renderer and feature support for the provider - // based on the feature flag - var certProvider render.CertificateProvider - var isWebhookSupportEnabled bool - if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { - certProvider = certproviders.CertManagerCertificateProvider{} - isWebhookSupportEnabled = true - } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { - certProvider = certproviders.OpenshiftServiceCaCertificateProvider{} - isWebhookSupportEnabled = true + var ctrlBuilderOpts []controllers.ControllerBuilderOption + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + ctrlBuilderOpts = append(ctrlBuilderOpts, controllers.WithOwns(&ocv1.ClusterExtensionRevision{})) } - // now initialize the helmApplier, assigning the potentially nil preAuth - helmApplier := &applier.Helm{ - ActionClientGetter: acg, - Preflights: preflights, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{ - BundleRenderer: registryv1.Renderer, - CertificateProvider: certProvider, - IsWebhookSupportEnabled: isWebhookSupportEnabled, - }, - PreAuthorizer: preAuth, + ceReconciler := &controllers.ClusterExtensionReconciler{ + Client: cl, + Resolver: resolver, + ImageCache: imageCache, + ImagePuller: imagePuller, + Finalizers: clusterExtensionFinalizers, } - - cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) - err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { - ext := obj.(*ocv1.ClusterExtension) - err := cm.Delete(ext) - return crfinalizer.Result{}, err - })) + ceController, err := ceReconciler.SetupWithManager(mgr, ctrlBuilderOpts...) if err != nil { - setupLog.Error(err, "unable to register content manager cleanup finalizer") + setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") return err } - if err = (&controllers.ClusterExtensionReconciler{ - Client: cl, - Resolver: resolver, - ImageCache: imageCache, - ImagePuller: imagePuller, - Applier: helmApplier, - InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, - Finalizers: clusterExtensionFinalizers, - Manager: cm, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") + certProvider := getCertificateProvider() + regv1ManifestProvider := &applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + CertificateProvider: certProvider, + IsWebhookSupportEnabled: certProvider != nil, + IsSingleOwnNamespaceEnabled: features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport), + } + + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + err = setupBoxcutter(mgr, ceReconciler, preflights, regv1ManifestProvider) + } else { + err = setupHelm(mgr, ceReconciler, preflights, ceController, clusterExtensionFinalizers, regv1ManifestProvider) + } + if err != nil { + setupLog.Error(err, "unable to setup lifecycler") return err } @@ -482,17 +478,16 @@ func run() error { return err } - if globalPullSecretKey != nil { - setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) - err := (&controllers.PullSecretReconciler{ - Client: mgr.GetClient(), - AuthFilePath: authFilePath, - SecretKey: *globalPullSecretKey, - }).SetupWithManager(mgr) - if err != nil { - setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") - return err - } + setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) + err = (&sharedcontrollers.PullSecretReconciler{ + Client: mgr.GetClient(), + AuthFilePath: authFilePath, + SecretKey: globalPullSecretKey, + ServiceAccountKey: saKey, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") + return err } //+kubebuilder:scaffold:builder @@ -519,6 +514,174 @@ func run() error { return nil } +func getCertificateProvider() render.CertificateProvider { + if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) { + return certproviders.CertManagerCertificateProvider{} + } else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) { + return certproviders.OpenshiftServiceCaCertificateProvider{} + } + return nil +} + +func setupBoxcutter( + mgr manager.Manager, + ceReconciler *controllers.ClusterExtensionReconciler, + preflights []applier.Preflight, + regv1ManifestProvider applier.ManifestProvider, +) error { + coreClient, err := corev1client.NewForConfig(mgr.GetConfig()) + if err != nil { + return fmt.Errorf("unable to create core client: %w", err) + } + cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), + helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)), + helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) { + ext := obj.(*ocv1.ClusterExtension) + return ext.Spec.Namespace, nil + }), + ) + if err != nil { + return fmt.Errorf("unable to create helm action config getter: %w", err) + } + + acg, err := action.NewWrappedActionClientGetter(cfgGetter, + helmclient.WithFailureRollbacks(false), + ) + if err != nil { + return fmt.Errorf("unable to create helm action client getter: %w", err) + } + + // TODO: add support for preflight checks + // TODO: better scheme handling - which types do we want to support? + _ = apiextensionsv1.AddToScheme(mgr.GetScheme()) + rg := &applier.SimpleRevisionGenerator{ + Scheme: mgr.GetScheme(), + ManifestProvider: regv1ManifestProvider, + } + ceReconciler.Applier = &applier.Boxcutter{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + RevisionGenerator: rg, + Preflights: preflights, + FieldOwner: fmt.Sprintf("%s/clusterextension-controller", fieldOwnerPrefix), + } + ceReconciler.RevisionStatesGetter = &controllers.BoxcutterRevisionStatesGetter{Reader: mgr.GetClient()} + ceReconciler.StorageMigrator = &applier.BoxcutterStorageMigrator{ + Client: mgr.GetClient(), + ActionClientGetter: acg, + RevisionGenerator: rg, + } + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) + if err != nil { + return fmt.Errorf("unable to create discovery client: %w", err) + } + + trackingCache, err := managedcache.NewTrackingCache( + ctrl.Log.WithName("trackingCache"), + mgr.GetConfig(), + crcache.Options{ + Scheme: mgr.GetScheme(), Mapper: mgr.GetRESTMapper(), + }, + ) + if err != nil { + return fmt.Errorf("unable to create boxcutter tracking cache: %v", err) + } + if err := mgr.Add(trackingCache); err != nil { + return fmt.Errorf("unable to add tracking cache to manager: %v", err) + } + + if err = (&controllers.ClusterExtensionRevisionReconciler{ + Client: mgr.GetClient(), + RevisionEngine: machinery.NewRevisionEngine( + machinery.NewPhaseEngine( + machinery.NewObjectEngine( + mgr.GetScheme(), trackingCache, mgr.GetClient(), + ownerhandling.NewNative(mgr.GetScheme()), + machinery.NewComparator(ownerhandling.NewNative(mgr.GetScheme()), discoveryClient, mgr.GetScheme(), fieldOwnerPrefix), + fieldOwnerPrefix, fieldOwnerPrefix, + ), + validation.NewClusterPhaseValidator(mgr.GetRESTMapper(), mgr.GetClient()), + ), + validation.NewRevisionValidator(), mgr.GetClient(), + ), + TrackingCache: trackingCache, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to setup ClusterExtensionRevision controller: %w", err) + } + return nil +} + +func setupHelm( + mgr manager.Manager, + ceReconciler *controllers.ClusterExtensionReconciler, + preflights []applier.Preflight, + ceController crcontroller.Controller, + clusterExtensionFinalizers crfinalizer.Registerer, + regv1ManifestProvider applier.ManifestProvider, +) error { + coreClient, err := corev1client.NewForConfig(mgr.GetConfig()) + if err != nil { + return fmt.Errorf("unable to create core client: %w", err) + } + tokenGetter := authentication.NewTokenGetter(coreClient, authentication.WithExpirationDuration(1*time.Hour)) + clientRestConfigMapper := action.ServiceAccountRestConfigMapper(tokenGetter) + if features.OperatorControllerFeatureGate.Enabled(features.SyntheticPermissions) { + clientRestConfigMapper = action.SyntheticUserRestConfigMapper(clientRestConfigMapper) + } + + cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), + helmclient.StorageDriverMapper(action.ChunkedStorageDriverMapper(coreClient, mgr.GetAPIReader(), cfg.systemNamespace)), + helmclient.ClientNamespaceMapper(func(obj client.Object) (string, error) { + ext := obj.(*ocv1.ClusterExtension) + return ext.Spec.Namespace, nil + }), + helmclient.ClientRestConfigMapper(clientRestConfigMapper), + ) + if err != nil { + return fmt.Errorf("unable to create helm action config getter: %w", err) + } + + acg, err := action.NewWrappedActionClientGetter(cfgGetter, + helmclient.WithFailureRollbacks(false), + ) + if err != nil { + return fmt.Errorf("unable to create helm action client getter: %w", err) + } + + // determine if PreAuthorizer should be enabled based on feature gate + var preAuth authorization.PreAuthorizer + if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { + preAuth = authorization.NewRBACPreAuthorizer(mgr.GetClient()) + } + + cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper()) + err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { + ext := obj.(*ocv1.ClusterExtension) + err := cm.Delete(ext) + return crfinalizer.Result{}, err + })) + if err != nil { + setupLog.Error(err, "unable to register content manager cleanup finalizer") + return err + } + + // now initialize the helmApplier, assigning the potentially nil preAuth + ceReconciler.Applier = &applier.Helm{ + ActionClientGetter: acg, + Preflights: preflights, + HelmChartProvider: &applier.RegistryV1HelmChartProvider{ + ManifestProvider: regv1ManifestProvider, + }, + HelmReleaseToObjectsConverter: &applier.HelmReleaseToObjectsConverter{}, + PreAuthorizer: preAuth, + Watcher: ceController, + Manager: cm, + } + ceReconciler.RevisionStatesGetter = &controllers.HelmRevisionStatesGetter{ActionClientGetter: acg} + return nil +} + func main() { if err := operatorControllerCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) diff --git a/codecov.yml b/codecov.yml index 32bd93c6ab..bbe044b0f5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,6 +1,8 @@ codecov: notify: - after_n_builds: 2 + # Configure the 4 builds to wait before sending a notification. + # test-unit, test-regression, test-e2e and test-experimental-e2e. + after_n_builds: 4 # Configure the paths to include in coverage reports. # Exclude documentation, YAML configurations, and test files. @@ -10,12 +12,16 @@ coverage: default: target: auto threshold: 2% + paths: + - "api/" + - "cmd/" + - "internal/" patch: default: target: auto threshold: 1% - paths: - - "api/" - - "cmd/" - - "internal/" + paths: + - "api/" + - "cmd/" + - "internal/" diff --git a/config/OWNERS b/config/OWNERS new file mode 100644 index 0000000000..b44dad0ea8 --- /dev/null +++ b/config/OWNERS @@ -0,0 +1,2 @@ +approvers: + - manifest-approvers diff --git a/config/README.md b/config/README.md index 449989b232..6fe4ab8892 100644 --- a/config/README.md +++ b/config/README.md @@ -1,77 +1,5 @@ -# OPERATOR-CONTROLLER CONFIG +# OPERATOR-CONTROLLER CONFIGURATION -## config/overlays/basic-olm +## Samples -This includes basic support for an insecure OLMv1 deployment. This configuration uses: -* config/base/catalogd -* config/base/operator-controller -* config/base/common - -## config/overlays/cert-manager - -This includes support for a secure (i.e. with TLS) configuration of OLMv1. This configuration uses: -* config/base/catalogd -* config/base/operator-controller -* config/base/common -* config/components/tls/catalogd -* config/components/tls/operator-controller -* config/components/tls/ca - -This configuration requires cert-manager. - -## config/overlays/e2e - -This provides additional configuration support for end-to-end testing, including code coverage. This configuration uses: -* config/base/catalogd -* config/base/operator-controller -* config/base/common -* config/components/coverage -* config/components/tls/catalogd -* config/components/tls/operator-controller -* config/components/tls/ca - -This configuration requires cert-manager. - -## Base Configuration - -The base configuration specifies a namespace of `olmv1-system`. - -### config/base/catalogd - -This provides the base configuration of catalogd. - -### config/base/operator-controller - -This provides the base configuration of operator-controller. - -### config/base/common - -This provides common components to both operator-controller and catalogd, i.e. namespace. - -## Components - -Each of the `kustomization.yaml` files specify a `Component`, rather than an overlay, and thus, can be used within the overlays. - -### config/components/tls/catalogd - -This provides a basic configuration of catalogd with TLS support. - -This component requires cert-manager. - -### config/components/tls/operator-controller - -This provides a basic configuration of operator-controller with TLS support for catalogd. - -This component requires cert-manager. - -### config/components/tls/ca - -Provides a CA for operator-controller/catalogd operation. - -This component _does not_ specify a namespace, and _must_ be included last. - -This component requires cert-manager. - -### config/components/coverage - -Provides configuration for code coverage. +The `config/samples` directory contains example ClusterCatalog and ClusterExtension resources. diff --git a/config/base/catalogd/crd/kustomization.yaml b/config/base/catalogd/crd/kustomization.yaml deleted file mode 100644 index 36c1512814..0000000000 --- a/config/base/catalogd/crd/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -- bases/olm.operatorframework.io_clustercatalogs.yaml -#+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/base/catalogd/kustomization.yaml b/config/base/catalogd/kustomization.yaml deleted file mode 100644 index 9a6bc25127..0000000000 --- a/config/base/catalogd/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: olmv1-system -namePrefix: catalogd- -resources: -- crd -- rbac -- manager diff --git a/config/base/catalogd/manager/kustomization.yaml b/config/base/catalogd/manager/kustomization.yaml deleted file mode 100644 index 2c10750df0..0000000000 --- a/config/base/catalogd/manager/kustomization.yaml +++ /dev/null @@ -1,18 +0,0 @@ -resources: -- manager.yaml -- service.yaml -- network_policy.yaml -- webhook/manifests.yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -images: -- name: controller - newName: quay.io/operator-framework/catalogd - newTag: devel -patches: -- path: webhook/patch.yaml - target: - group: admissionregistration.k8s.io - kind: MutatingWebhookConfiguration - name: mutating-webhook-configuration - version: v1 diff --git a/config/base/catalogd/manager/manager.yaml b/config/base/catalogd/manager/manager.yaml deleted file mode 100644 index 5c52165ec6..0000000000 --- a/config/base/catalogd/manager/manager.yaml +++ /dev/null @@ -1,82 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system - annotations: - kubectl.kubernetes.io/default-logs-container: manager - labels: - control-plane: catalogd-controller-manager -spec: - selector: - matchLabels: - control-plane: catalogd-controller-manager - replicas: 1 - minReadySeconds: 5 - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - labels: - control-plane: catalogd-controller-manager - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/arch - operator: In - values: - - amd64 - - arm64 - - ppc64le - - s390x - - key: kubernetes.io/os - operator: In - values: - - linux - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - command: - - ./catalogd - args: - - --leader-elect - - --metrics-bind-address=:7443 - - --external-address=catalogd-service.olmv1-system.svc - image: controller:latest - name: manager - volumeMounts: - - name: cache - mountPath: /var/cache/ - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - requests: - cpu: 100m - memory: 200Mi - imagePullPolicy: IfNotPresent - terminationMessagePolicy: FallbackToLogsOnError - serviceAccountName: controller-manager - terminationGracePeriodSeconds: 10 - volumes: - - name: cache - emptyDir: {} diff --git a/config/base/catalogd/manager/network_policy.yaml b/config/base/catalogd/manager/network_policy.yaml deleted file mode 100644 index 853b54a372..0000000000 --- a/config/base/catalogd/manager/network_policy.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: controller-manager - namespace: system -spec: - podSelector: - matchLabels: - control-plane: catalogd-controller-manager - policyTypes: - - Ingress - - Egress - ingress: - - ports: - - protocol: TCP - port: 7443 # metrics - - protocol: TCP - port: 8443 # catalogd http server - - protocol: TCP - port: 9443 # webhook - egress: - - {} # Allows all egress traffic (needed to pull catalog images from arbitrary image registries) diff --git a/config/base/catalogd/manager/service.yaml b/config/base/catalogd/manager/service.yaml deleted file mode 100644 index 693b687f39..0000000000 --- a/config/base/catalogd/manager/service.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: service - namespace: system -spec: - selector: - control-plane: catalogd-controller-manager - ports: - - name: http - protocol: TCP - port: 80 - targetPort: 8443 - - name: webhook - protocol: TCP - port: 9443 - targetPort: 9443 - - name: metrics - protocol: TCP - port: 7443 - targetPort: 7443 diff --git a/config/base/catalogd/manager/webhook/manifests.yaml b/config/base/catalogd/manager/webhook/manifests.yaml deleted file mode 100644 index a5842de425..0000000000 --- a/config/base/catalogd/manager/webhook/manifests.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: mutating-webhook-configuration -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: webhook-service - namespace: system - path: /mutate-olm-operatorframework-io-v1-clustercatalog - failurePolicy: Fail - name: inject-metadata-name.olm.operatorframework.io - rules: - - apiGroups: - - olm.operatorframework.io - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - clustercatalogs - sideEffects: None - timeoutSeconds: 10 diff --git a/config/base/catalogd/manager/webhook/patch.yaml b/config/base/catalogd/manager/webhook/patch.yaml deleted file mode 100644 index ab8528c765..0000000000 --- a/config/base/catalogd/manager/webhook/patch.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# None of these values can be set via the kubebuilder directive, hence this patch -- op: replace - path: /webhooks/0/clientConfig/service/namespace - value: olmv1-system -- op: replace - path: /webhooks/0/clientConfig/service/name - value: catalogd-service -- op: add - path: /webhooks/0/clientConfig/service/port - value: 9443 -# Make sure there's a name defined, otherwise, we can't create a label. This could happen when generateName is set -# Then, if any of the conditions are true, create the label: -# 1. No labels exist -# 2. The olm.operatorframework.io/metadata.name label doesn't exist -# 3. The olm.operatorframework.io/metadata.name label doesn't match the name -- op: add - path: /webhooks/0/matchConditions - value: - - name: MissingOrIncorrectMetadataNameLabel - expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" diff --git a/config/base/catalogd/rbac/auth_proxy_client_clusterrole.yaml b/config/base/catalogd/rbac/auth_proxy_client_clusterrole.yaml deleted file mode 100644 index ab8871b2e5..0000000000 --- a/config/base/catalogd/rbac/auth_proxy_client_clusterrole.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: metrics-reader -rules: -- nonResourceURLs: - - "/metrics" - verbs: - - get diff --git a/config/base/catalogd/rbac/auth_proxy_role.yaml b/config/base/catalogd/rbac/auth_proxy_role.yaml deleted file mode 100644 index 3edf78f589..0000000000 --- a/config/base/catalogd/rbac/auth_proxy_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: proxy-role -rules: -- apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create diff --git a/config/base/catalogd/rbac/auth_proxy_role_binding.yaml b/config/base/catalogd/rbac/auth_proxy_role_binding.yaml deleted file mode 100644 index 2efcf8dd81..0000000000 --- a/config/base/catalogd/rbac/auth_proxy_role_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: proxy-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: proxy-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/base/catalogd/rbac/kustomization.yaml b/config/base/catalogd/rbac/kustomization.yaml deleted file mode 100644 index 8ed66bdd16..0000000000 --- a/config/base/catalogd/rbac/kustomization.yaml +++ /dev/null @@ -1,20 +0,0 @@ -resources: -# All RBAC will be applied under this service account in -# the deployment namespace. You may comment out this resource -# if your manager will use a service account that exists at -# runtime. Be sure to update RoleBinding and ClusterRoleBinding -# subjects if changing service account names. -- service_account.yaml -- role.yaml -- role_binding.yaml -- leader_election_role.yaml -- leader_election_role_binding.yaml -# The following RBAC configurations are used to protect -# the metrics endpoint with authn/authz. These configurations -# ensure that only authorized users and service accounts -# can access the metrics endpoint. Comment the following -# permissions if you want to disable this protection. -# More info: https://book.kubebuilder.io/reference/metrics.html -- auth_proxy_role.yaml -- auth_proxy_role_binding.yaml -- auth_proxy_client_clusterrole.yaml diff --git a/config/base/catalogd/rbac/leader_election_role.yaml b/config/base/catalogd/rbac/leader_election_role.yaml deleted file mode 100644 index 37564d0841..0000000000 --- a/config/base/catalogd/rbac/leader_election_role.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# permissions to do leader election. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: leader-election-role -rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch diff --git a/config/base/catalogd/rbac/leader_election_role_binding.yaml b/config/base/catalogd/rbac/leader_election_role_binding.yaml deleted file mode 100644 index 6ad0ccf99a..0000000000 --- a/config/base/catalogd/rbac/leader_election_role_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: leader-election-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: leader-election-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/base/catalogd/rbac/role.yaml b/config/base/catalogd/rbac/role.yaml deleted file mode 100644 index 40f4095c62..0000000000 --- a/config/base/catalogd/rbac/role.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: manager-role -rules: -- apiGroups: - - olm.operatorframework.io - resources: - - clustercatalogs - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - olm.operatorframework.io - resources: - - clustercatalogs/finalizers - verbs: - - update -- apiGroups: - - olm.operatorframework.io - resources: - - clustercatalogs/status - verbs: - - get - - patch - - update diff --git a/config/base/catalogd/rbac/role_binding.yaml b/config/base/catalogd/rbac/role_binding.yaml deleted file mode 100644 index a618c0e474..0000000000 --- a/config/base/catalogd/rbac/role_binding.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: manager-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/base/catalogd/rbac/service_account.yaml b/config/base/catalogd/rbac/service_account.yaml deleted file mode 100644 index 3f0e7af74d..0000000000 --- a/config/base/catalogd/rbac/service_account.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/part-of: olm - app.kubernetes.io/name: catalogd - name: controller-manager - namespace: system diff --git a/config/base/common/kustomization.yaml b/config/base/common/kustomization.yaml deleted file mode 100644 index be904a9abc..0000000000 --- a/config/base/common/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- namespace.yaml -- network_policy.yaml diff --git a/config/base/common/namespace.yaml b/config/base/common/namespace.yaml deleted file mode 100644 index 99d47415f2..0000000000 --- a/config/base/common/namespace.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - labels: - app.kubernetes.io/part-of: olm - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/enforce-version: latest - name: system diff --git a/config/base/operator-controller/crd/kustomization.yaml b/config/base/operator-controller/crd/kustomization.yaml deleted file mode 100644 index ec864639d2..0000000000 --- a/config/base/operator-controller/crd/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -- bases/olm.operatorframework.io_clusterextensions.yaml - -# the following config is for teaching kustomize how to do kustomization for CRDs. -configurations: -- kustomizeconfig.yaml diff --git a/config/base/operator-controller/crd/kustomizeconfig.yaml b/config/base/operator-controller/crd/kustomizeconfig.yaml deleted file mode 100644 index ec5c150a9d..0000000000 --- a/config/base/operator-controller/crd/kustomizeconfig.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# This file is for teaching kustomize how to substitute name and namespace reference in CRD -nameReference: -- kind: Service - version: v1 - fieldSpecs: - - kind: CustomResourceDefinition - version: v1 - group: apiextensions.k8s.io - path: spec/conversion/webhook/clientConfig/service/name - -namespace: -- kind: CustomResourceDefinition - version: v1 - group: apiextensions.k8s.io - path: spec/conversion/webhook/clientConfig/service/namespace - create: false - -varReference: -- path: metadata/annotations diff --git a/config/base/operator-controller/kustomization.yaml b/config/base/operator-controller/kustomization.yaml deleted file mode 100644 index 1d63fb17fa..0000000000 --- a/config/base/operator-controller/kustomization.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: olmv1-system -namePrefix: operator-controller- -resources: -- crd -- rbac -- manager - diff --git a/config/base/operator-controller/manager/kustomization.yaml b/config/base/operator-controller/manager/kustomization.yaml deleted file mode 100644 index 259f17c9e4..0000000000 --- a/config/base/operator-controller/manager/kustomization.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- manager.yaml -- service.yaml -- network_policy.yaml - -images: -- name: controller - newName: quay.io/operator-framework/operator-controller - newTag: devel diff --git a/config/base/operator-controller/manager/manager.yaml b/config/base/operator-controller/manager/manager.yaml deleted file mode 100644 index db34940c37..0000000000 --- a/config/base/operator-controller/manager/manager.yaml +++ /dev/null @@ -1,83 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: controller-manager - namespace: system - annotations: - kubectl.kubernetes.io/default-logs-container: manager - labels: - control-plane: operator-controller-controller-manager -spec: - selector: - matchLabels: - control-plane: operator-controller-controller-manager - replicas: 1 - template: - metadata: - annotations: - kubectl.kubernetes.io/default-container: manager - labels: - control-plane: operator-controller-controller-manager - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/arch - operator: In - values: - - amd64 - - arm64 - - ppc64le - - s390x - - key: kubernetes.io/os - operator: In - values: - - linux - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - command: - - /operator-controller - args: - - "--health-probe-bind-address=:8081" - - "--metrics-bind-address=:8443" - - "--leader-elect" - image: controller:latest - imagePullPolicy: IfNotPresent - name: manager - volumeMounts: - - name: cache - mountPath: /var/cache - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - # TODO(user): Configure the resources accordingly based on the project requirements. - # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - resources: - requests: - cpu: 10m - memory: 64Mi - terminationMessagePolicy: FallbackToLogsOnError - serviceAccountName: operator-controller-controller-manager - terminationGracePeriodSeconds: 10 - volumes: - - name: cache - emptyDir: {} diff --git a/config/base/operator-controller/manager/network_policy.yaml b/config/base/operator-controller/manager/network_policy.yaml deleted file mode 100644 index 2e68beabee..0000000000 --- a/config/base/operator-controller/manager/network_policy.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: controller-manager - namespace: system -spec: - podSelector: - matchLabels: - control-plane: operator-controller-controller-manager - policyTypes: - - Ingress - - Egress - ingress: - - ports: - - protocol: TCP - port: 8443 # metrics - egress: - - {} # Allows all egress traffic (needed to pull bundle images from arbitrary image registries) diff --git a/config/base/operator-controller/manager/service.yaml b/config/base/operator-controller/manager/service.yaml deleted file mode 100644 index b352a0aa1b..0000000000 --- a/config/base/operator-controller/manager/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: service - namespace: system - labels: - control-plane: operator-controller-controller-manager -spec: - ports: - - name: https - port: 8443 - protocol: TCP - targetPort: 8443 - selector: - control-plane: operator-controller-controller-manager diff --git a/config/base/operator-controller/rbac/auth_proxy_client_clusterrole.yaml b/config/base/operator-controller/rbac/auth_proxy_client_clusterrole.yaml deleted file mode 100644 index 51a75db47a..0000000000 --- a/config/base/operator-controller/rbac/auth_proxy_client_clusterrole.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: metrics-reader -rules: -- nonResourceURLs: - - "/metrics" - verbs: - - get diff --git a/config/base/operator-controller/rbac/auth_proxy_role.yaml b/config/base/operator-controller/rbac/auth_proxy_role.yaml deleted file mode 100644 index 80e1857c59..0000000000 --- a/config/base/operator-controller/rbac/auth_proxy_role.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: proxy-role -rules: -- apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create -- apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create diff --git a/config/base/operator-controller/rbac/clusterextension_editor_role.yaml b/config/base/operator-controller/rbac/clusterextension_editor_role.yaml deleted file mode 100644 index 61cd61ce3b..0000000000 --- a/config/base/operator-controller/rbac/clusterextension_editor_role.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# permissions for end users to edit cluster extensions. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: clusterextension-editor-role -rules: -- apiGroups: - - olm.operatorframework.io - resources: - - clusterextensions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch diff --git a/config/base/operator-controller/rbac/clusterextension_viewer_role.yaml b/config/base/operator-controller/rbac/clusterextension_viewer_role.yaml deleted file mode 100644 index bee8b9d9ea..0000000000 --- a/config/base/operator-controller/rbac/clusterextension_viewer_role.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# permissions for end users to view cluster extensions. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: clusterextension-viewer-role -rules: -- apiGroups: - - olm.operatorframework.io - resources: - - clusterextensions - verbs: - - get - - list - - watch diff --git a/config/base/operator-controller/rbac/kustomization.yaml b/config/base/operator-controller/rbac/kustomization.yaml deleted file mode 100644 index 719df5654f..0000000000 --- a/config/base/operator-controller/rbac/kustomization.yaml +++ /dev/null @@ -1,27 +0,0 @@ -resources: -# All RBAC will be applied under this service account in -# the deployment namespace. You may comment out this resource -# if your manager will use a service account that exists at -# runtime. Be sure to update RoleBinding and ClusterRoleBinding -# subjects if changing service account names. -- service_account.yaml -- role.yaml -- role_binding.yaml -- leader_election_role.yaml -- leader_election_role_binding.yaml - -# The following resources are pre-defined roles for editors and viewers -# of APIs provided by this project. -- clusterextension_editor_role.yaml -- clusterextension_viewer_role.yaml - -# The following RBAC configurations are used to protect -# the metrics endpoint with authn/authz. These configurations -# ensure that only authorized users and service accounts -# can access the metrics endpoint. Comment the following -# permissions if you want to disable this protection. -# More info: https://book.kubebuilder.io/reference/metrics.html -- auth_proxy_role.yaml -- auth_proxy_role_binding.yaml -- auth_proxy_client_clusterrole.yaml - diff --git a/config/base/operator-controller/rbac/leader_election_role.yaml b/config/base/operator-controller/rbac/leader_election_role.yaml deleted file mode 100644 index 4190ec8059..0000000000 --- a/config/base/operator-controller/rbac/leader_election_role.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# permissions to do leader election. -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: leader-election-role -rules: -- apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete -- apiGroups: - - "" - resources: - - events - verbs: - - create - - patch diff --git a/config/base/operator-controller/rbac/leader_election_role_binding.yaml b/config/base/operator-controller/rbac/leader_election_role_binding.yaml deleted file mode 100644 index 1d1321ed4f..0000000000 --- a/config/base/operator-controller/rbac/leader_election_role_binding.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: leader-election-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: leader-election-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/base/operator-controller/rbac/role.yaml b/config/base/operator-controller/rbac/role.yaml deleted file mode 100644 index be89deec1d..0000000000 --- a/config/base/operator-controller/rbac/role.yaml +++ /dev/null @@ -1,79 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: manager-role -rules: -- apiGroups: - - "" - resources: - - serviceaccounts/token - verbs: - - create -- apiGroups: - - apiextensions.k8s.io - resources: - - customresourcedefinitions - verbs: - - get -- apiGroups: - - olm.operatorframework.io - resources: - - clustercatalogs - verbs: - - get - - list - - watch -- apiGroups: - - olm.operatorframework.io - resources: - - clusterextensions - verbs: - - get - - list - - patch - - update - - watch -- apiGroups: - - olm.operatorframework.io - resources: - - clusterextensions/finalizers - verbs: - - update -- apiGroups: - - olm.operatorframework.io - resources: - - clusterextensions/status - verbs: - - patch - - update -- apiGroups: - - rbac.authorization.k8s.io - resources: - - clusterrolebindings - - clusterroles - - rolebindings - - roles - verbs: - - list - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: manager-role - namespace: system -rules: -- apiGroups: - - "" - resources: - - secrets - verbs: - - create - - delete - - deletecollection - - get - - list - - patch - - update - - watch diff --git a/config/base/operator-controller/rbac/role_binding.yaml b/config/base/operator-controller/rbac/role_binding.yaml deleted file mode 100644 index fa331e3d41..0000000000 --- a/config/base/operator-controller/rbac/role_binding.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: manager-rolebinding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: manager-role -subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: manager-rolebinding - namespace: system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: manager-role -subjects: - - kind: ServiceAccount - name: controller-manager - namespace: system diff --git a/config/base/operator-controller/rbac/service_account.yaml b/config/base/operator-controller/rbac/service_account.yaml deleted file mode 100644 index 7cd6025bfc..0000000000 --- a/config/base/operator-controller/rbac/service_account.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: controller-manager - namespace: system diff --git a/config/catalogs/nginx-ingress/kustomization.yaml b/config/catalogs/nginx-ingress/kustomization.yaml deleted file mode 100644 index 7bdced5d67..0000000000 --- a/config/catalogs/nginx-ingress/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- ../default -- resources/nginx_ingress.yaml -- https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml diff --git a/config/catalogs/nginx-ingress/resources/nginx_ingress.yaml b/config/catalogs/nginx-ingress/resources/nginx_ingress.yaml deleted file mode 100644 index 81f775fba1..0000000000 --- a/config/catalogs/nginx-ingress/resources/nginx_ingress.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: catalogd-ingress - namespace: olmv1-system -spec: - ingressClassName: nginx - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: catalogd-service - port: - number: 80 diff --git a/config/components/coverage/kustomization.yaml b/config/components/coverage/kustomization.yaml deleted file mode 100644 index 5522eb7f83..0000000000 --- a/config/components/coverage/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component -namespace: olmv1-system -resources: -- manager_e2e_coverage_pvc.yaml -- manager_e2e_coverage_copy_pod.yaml -patches: -- path: manager_e2e_coverage_patch.yaml diff --git a/config/components/coverage/manager_e2e_coverage_copy_pod.yaml b/config/components/coverage/manager_e2e_coverage_copy_pod.yaml deleted file mode 100644 index 7794ba97d6..0000000000 --- a/config/components/coverage/manager_e2e_coverage_copy_pod.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: e2e-coverage-copy-pod -spec: - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 65532 - seccompProfile: - type: RuntimeDefault - containers: - - name: tar - image: busybox:1.36 - command: ["sleep", "infinity"] - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - "ALL" - terminationMessagePolicy: FallbackToLogsOnError - volumeMounts: - - name: e2e-coverage-volume - mountPath: /e2e-coverage - readOnly: true - volumes: - - name: e2e-coverage-volume - persistentVolumeClaim: - claimName: e2e-coverage - readOnly: true diff --git a/config/components/coverage/manager_e2e_coverage_patch.yaml b/config/components/coverage/manager_e2e_coverage_patch.yaml deleted file mode 100644 index 171a1607cf..0000000000 --- a/config/components/coverage/manager_e2e_coverage_patch.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: operator-controller-controller-manager - namespace: olmv1-system -spec: - template: - spec: - containers: - - name: manager - env: - - name: GOCOVERDIR - value: /e2e-coverage - volumeMounts: - - name: e2e-coverage-volume - mountPath: /e2e-coverage - volumes: - - name: e2e-coverage-volume - persistentVolumeClaim: - claimName: e2e-coverage diff --git a/config/components/coverage/manager_e2e_coverage_pvc.yaml b/config/components/coverage/manager_e2e_coverage_pvc.yaml deleted file mode 100644 index 126d4d4e63..0000000000 --- a/config/components/coverage/manager_e2e_coverage_pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: e2e-coverage -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 64Mi diff --git a/config/components/registries-conf/kustomization.yaml b/config/components/registries-conf/kustomization.yaml deleted file mode 100644 index e482624290..0000000000 --- a/config/components/registries-conf/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component -namespace: olmv1-system -resources: -- registries_conf_configmap.yaml -patches: -- path: manager_e2e_registries_conf_patch.yaml diff --git a/config/components/registries-conf/manager_e2e_registries_conf_patch.yaml b/config/components/registries-conf/manager_e2e_registries_conf_patch.yaml deleted file mode 100644 index aa08a3d245..0000000000 --- a/config/components/registries-conf/manager_e2e_registries_conf_patch.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: operator-controller-controller-manager - namespace: olmv1-system -spec: - template: - spec: - containers: - - name: manager - volumeMounts: - - name: e2e-registries-conf - mountPath: /etc/containers - volumes: - - name: e2e-registries-conf - configMap: - name: e2e-registries-conf diff --git a/config/components/tls/ca/kustomization.yaml b/config/components/tls/ca/kustomization.yaml deleted file mode 100644 index 5cbe13ad21..0000000000 --- a/config/components/tls/ca/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component -# No namespace is specified here, otherwise, it will overwrite _all_ the other namespaces! -resources: -- issuers.yaml diff --git a/config/components/tls/catalogd/kustomization.yaml b/config/components/tls/catalogd/kustomization.yaml deleted file mode 100644 index f603a00996..0000000000 --- a/config/components/tls/catalogd/kustomization.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component -namespace: olmv1-system -resources: -- resources/certificate.yaml -patches: -- target: - kind: Service - labelSelector: app.kubernetes.io/name=catalogd - path: patches/catalogd_service_port.yaml -- target: - kind: Deployment - labelSelector: control-plane=catalogd-controller-manager - path: patches/manager_deployment_certs.yaml -- target: - kind: Deployment - labelSelector: control-plane=catalogd-controller-manager - path: patches/manager_deployment_cacerts.yaml -- target: - group: admissionregistration.k8s.io - kind: MutatingWebhookConfiguration - name: mutating-webhook-configuration - version: v1 - path: patches/catalogd_webhook.yaml diff --git a/config/components/tls/catalogd/patches/catalogd_service_port.yaml b/config/components/tls/catalogd/patches/catalogd_service_port.yaml deleted file mode 100644 index b5b88bb47e..0000000000 --- a/config/components/tls/catalogd/patches/catalogd_service_port.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- op: replace - path: /spec/ports/0/port - value: 443 -- op: replace - path: /spec/ports/0/name - value: https \ No newline at end of file diff --git a/config/components/tls/catalogd/patches/catalogd_webhook.yaml b/config/components/tls/catalogd/patches/catalogd_webhook.yaml deleted file mode 100644 index cf1a39ec33..0000000000 --- a/config/components/tls/catalogd/patches/catalogd_webhook.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- op: add - path: /metadata/annotations/cert-manager.io~1inject-ca-from-secret - value: cert-manager/olmv1-ca diff --git a/config/components/tls/catalogd/patches/manager_deployment_cacerts.yaml b/config/components/tls/catalogd/patches/manager_deployment_cacerts.yaml deleted file mode 100644 index 6b0816706b..0000000000 --- a/config/components/tls/catalogd/patches/manager_deployment_cacerts.yaml +++ /dev/null @@ -1,9 +0,0 @@ -- op: add - path: /spec/template/spec/volumes/- - value: {"name":"olmv1-certificate", "secret":{"secretName":"catalogd-service-cert-git-version", "optional": false, "items": [{"key": "ca.crt", "path": "olm-ca.crt"}]}} -- op: add - path: /spec/template/spec/containers/0/volumeMounts/- - value: {"name":"olmv1-certificate", "readOnly": true, "mountPath":"/var/ca-certs/"} -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--pull-cas-dir=/var/ca-certs" diff --git a/config/components/tls/catalogd/patches/manager_deployment_certs.yaml b/config/components/tls/catalogd/patches/manager_deployment_certs.yaml deleted file mode 100644 index 3d8b33ac3d..0000000000 --- a/config/components/tls/catalogd/patches/manager_deployment_certs.yaml +++ /dev/null @@ -1,12 +0,0 @@ -- op: add - path: /spec/template/spec/volumes/- - value: {"name":"catalogserver-certs", "secret":{"secretName":"catalogd-service-cert-git-version"}} -- op: add - path: /spec/template/spec/containers/0/volumeMounts/- - value: {"name":"catalogserver-certs", "mountPath":"/var/certs"} -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--tls-cert=/var/certs/tls.crt" -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--tls-key=/var/certs/tls.key" diff --git a/config/components/tls/catalogd/resources/certificate.yaml b/config/components/tls/catalogd/resources/certificate.yaml deleted file mode 100644 index cacb0bc9b2..0000000000 --- a/config/components/tls/catalogd/resources/certificate.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: catalogd-service-cert - namespace: system -spec: - secretName: catalogd-service-cert-git-version - dnsNames: - - localhost - - catalogd-service.olmv1-system.svc - - catalogd-service.olmv1-system.svc.cluster.local - privateKey: - algorithm: ECDSA - size: 256 - issuerRef: - kind: ClusterIssuer - group: cert-manager.io - name: olmv1-ca diff --git a/config/components/tls/operator-controller/kustomization.yaml b/config/components/tls/operator-controller/kustomization.yaml deleted file mode 100644 index 6c4e13975b..0000000000 --- a/config/components/tls/operator-controller/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1alpha1 -kind: Component -namespace: olmv1-system -resources: -- resources/manager_cert.yaml -patches: -- target: - kind: Deployment - name: controller-manager - labelSelector: control-plane=operator-controller-controller-manager - path: patches/manager_deployment_cert.yaml diff --git a/config/components/tls/operator-controller/patches/manager_deployment_cert.yaml b/config/components/tls/operator-controller/patches/manager_deployment_cert.yaml deleted file mode 100644 index 8fbdb55927..0000000000 --- a/config/components/tls/operator-controller/patches/manager_deployment_cert.yaml +++ /dev/null @@ -1,18 +0,0 @@ -- op: add - path: /spec/template/spec/volumes/- - value: {"name":"olmv1-certificate", "secret":{"secretName":"olmv1-cert", "optional": false, "items": [{"key": "ca.crt", "path": "olm-ca.crt"}, {"key": "tls.crt", "path": "tls.cert"}, {"key": "tls.key", "path": "tls.key"}]}} -- op: add - path: /spec/template/spec/containers/0/volumeMounts/- - value: {"name":"olmv1-certificate", "readOnly": true, "mountPath":"/var/certs/"} -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--catalogd-cas-dir=/var/certs" -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--pull-cas-dir=/var/certs" -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--tls-cert=/var/certs/tls.cert" -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--tls-key=/var/certs/tls.key" diff --git a/config/components/tls/operator-controller/resources/manager_cert.yaml b/config/components/tls/operator-controller/resources/manager_cert.yaml deleted file mode 100644 index 96f131b7e2..0000000000 --- a/config/components/tls/operator-controller/resources/manager_cert.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: olmv1-cert -spec: - secretName: olmv1-cert - dnsNames: - - operator-controller-service.olmv1-system.svc - - operator-controller-service.olmv1-system.svc.cluster.local - privateKey: - algorithm: ECDSA - size: 256 - issuerRef: - name: olmv1-ca - kind: ClusterIssuer - group: cert-manager.io diff --git a/config/overlays/basic-olm/kustomization.yaml b/config/overlays/basic-olm/kustomization.yaml deleted file mode 100644 index 5975b3c046..0000000000 --- a/config/overlays/basic-olm/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# kustomization file for based, non-secure OLMv1 -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../../base/catalogd -- ../../base/operator-controller -- ../../base/common diff --git a/config/overlays/cert-manager/kustomization.yaml b/config/overlays/cert-manager/kustomization.yaml deleted file mode 100644 index ea113bb9d3..0000000000 --- a/config/overlays/cert-manager/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# kustomization file for secure OLMv1 -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../../base/catalogd -- ../../base/operator-controller -- ../../base/common -components: -- ../../components/tls/catalogd -- ../../components/tls/operator-controller -# ca must be last other components will overwrite the namespaces -- ../../components/tls/ca diff --git a/config/overlays/e2e/kustomization.yaml b/config/overlays/e2e/kustomization.yaml deleted file mode 100644 index bc83e9fd3b..0000000000 --- a/config/overlays/e2e/kustomization.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# kustomization file for all the e2e's -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../../base/catalogd -- ../../base/operator-controller -- ../../base/common -components: -- ../../components/tls/catalogd -- ../../components/tls/operator-controller -- ../../components/coverage -- ../../components/registries-conf -# ca must be last or other components will overwrite the namespaces -- ../../components/tls/ca diff --git a/config/overlays/featuregate/synthetic-user-permissions/kustomization.yaml b/config/overlays/featuregate/synthetic-user-permissions/kustomization.yaml deleted file mode 100644 index 01e3a6d0ea..0000000000 --- a/config/overlays/featuregate/synthetic-user-permissions/kustomization.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# kustomization file for secure OLMv1 -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../../base/operator-controller - - ../../../base/common -components: - - ../../../components/tls/operator-controller - -patches: - - target: - kind: Deployment - name: operator-controller-controller-manager - path: patches/enable-featuregate.yaml - - target: - kind: ClusterRole - name: operator-controller-manager-role - path: patches/impersonate-perms.yaml diff --git a/config/overlays/featuregate/synthetic-user-permissions/patches/enable-featuregate.yaml b/config/overlays/featuregate/synthetic-user-permissions/patches/enable-featuregate.yaml deleted file mode 100644 index fb6c84fa4f..0000000000 --- a/config/overlays/featuregate/synthetic-user-permissions/patches/enable-featuregate.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# enable synthetic-user feature gate -- op: add - path: /spec/template/spec/containers/0/args/- - value: "--feature-gates=SyntheticPermissions=true" diff --git a/config/overlays/featuregate/synthetic-user-permissions/patches/impersonate-perms.yaml b/config/overlays/featuregate/synthetic-user-permissions/patches/impersonate-perms.yaml deleted file mode 100644 index f3854ea2ae..0000000000 --- a/config/overlays/featuregate/synthetic-user-permissions/patches/impersonate-perms.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# enable synthetic-user feature gate -- op: add - path: /rules/- - value: - apiGroups: - - "" - resources: - - groups - - users - verbs: - - impersonate diff --git a/config/overlays/tilt-local-dev/catalogd/kustomization.yaml b/config/overlays/tilt-local-dev/catalogd/kustomization.yaml deleted file mode 100644 index 846656bb43..0000000000 --- a/config/overlays/tilt-local-dev/catalogd/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# kustomization file for secure operator-controller -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../../../base/catalogd -- ../../../base/common -components: -- ../../../components/tls/catalogd -# ca must be last or other components will overwrite the namespaces -- ../../../components/tls/ca - -patches: - - target: - kind: Deployment - name: catalogd-controller-manager - path: patches/dev-deployment.yaml diff --git a/config/overlays/tilt-local-dev/catalogd/patches/dev-deployment.yaml b/config/overlays/tilt-local-dev/catalogd/patches/dev-deployment.yaml deleted file mode 100644 index 4df9069212..0000000000 --- a/config/overlays/tilt-local-dev/catalogd/patches/dev-deployment.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# remove livenessProbe and readinessProbe so container doesn't restart during breakpoints -- op: replace - path: /spec/template/spec/containers/0/livenessProbe - value: null -- op: replace - path: /spec/template/spec/containers/0/readinessProbe - value: null -- op: remove - # remove --leader-elect so container doesn't restart during breakpoints - path: /spec/template/spec/containers/0/args/0 diff --git a/config/overlays/tilt-local-dev/operator-controller/kustomization.yaml b/config/overlays/tilt-local-dev/operator-controller/kustomization.yaml deleted file mode 100644 index 403f2d102c..0000000000 --- a/config/overlays/tilt-local-dev/operator-controller/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# kustomization file for secure operator-controller -# DO NOT ADD A NAMESPACE HERE -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- ../../../base/operator-controller -- ../../../base/common -components: -- ../../../components/tls/operator-controller -# ca must be last or other components will overwrite the namespaces -- ../../../components/tls/ca - -patches: - - target: - kind: Deployment - name: operator-controller-controller-manager - path: patches/dev-deployment.yaml diff --git a/config/overlays/tilt-local-dev/operator-controller/patches/dev-deployment.yaml b/config/overlays/tilt-local-dev/operator-controller/patches/dev-deployment.yaml deleted file mode 100644 index b273a0c9ba..0000000000 --- a/config/overlays/tilt-local-dev/operator-controller/patches/dev-deployment.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# remove livenessProbe and readinessProbe so container doesn't restart during breakpoints -- op: replace - path: /spec/template/spec/containers/0/livenessProbe - value: null -- op: replace - path: /spec/template/spec/containers/0/readinessProbe - value: null -- op: remove - # remove --leader-elect so container doesn't restart during breakpoints - path: /spec/template/spec/containers/0/args/2 diff --git a/config/samples/olm_v1_clusterextension.yaml b/config/samples/olm_v1_clusterextension.yaml index 4438dfb76d..14c8e167e0 100644 --- a/config/samples/olm_v1_clusterextension.yaml +++ b/config/samples/olm_v1_clusterextension.yaml @@ -33,6 +33,10 @@ rules: resources: [clusterextensions/finalizers] verbs: [update] resourceNames: [argocd] +# Allow ClusterExtensionRevisions to set blockOwnerDeletion ownerReferences +- apiGroups: [olm.operatorframework.io] + resources: [clusterextensionrevisions/finalizers] + verbs: [update] # Manage ArgoCD CRDs - apiGroups: [apiextensions.k8s.io] resources: [customresourcedefinitions] diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml deleted file mode 100644 index a5842de425..0000000000 --- a/config/webhook/manifests.yaml +++ /dev/null @@ -1,27 +0,0 @@ ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: mutating-webhook-configuration -webhooks: -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: webhook-service - namespace: system - path: /mutate-olm-operatorframework-io-v1-clustercatalog - failurePolicy: Fail - name: inject-metadata-name.olm.operatorframework.io - rules: - - apiGroups: - - olm.operatorframework.io - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - clustercatalogs - sideEffects: None - timeoutSeconds: 10 diff --git a/docs/OWNERS b/docs/OWNERS index d1a8d41f19..342c5f6315 100644 --- a/docs/OWNERS +++ b/docs/OWNERS @@ -1,23 +1,2 @@ approvers: - # contributors who can approve any docs PRs in the repo - - michaelryanpeter -reviewers: - # contributors who can review/lgtm any docs PRs in the repo - - anik120 - - ankitathomas - - bentito - - camilamacedo86 - - dtfranz - - gallettilance - - gavinmbell - - grokspawn - - joelanford - - kevinrizza - - LalatenduMohanty - - oceanc80 - - OchiengEd - - perdasilva - - rashmigottipati - - thetechnick - - tmshort - - trgeiger + - docs-approvers diff --git a/docs/api-reference/crd-ref-docs-gen-config.yaml b/docs/api-reference/crd-ref-docs-gen-config.yaml index f6394fdf61..c8efa15c19 100644 --- a/docs/api-reference/crd-ref-docs-gen-config.yaml +++ b/docs/api-reference/crd-ref-docs-gen-config.yaml @@ -1,5 +1,5 @@ processor: - ignoreTypes: [] + ignoreTypes: [ClusterExtensionRevision, ClusterExtensionRevisionList] ignoreFields: [] render: diff --git a/docs/api-reference/operator-controller-api-reference.md b/docs/api-reference/olmv1-api-reference.md similarity index 95% rename from docs/api-reference/operator-controller-api-reference.md rename to docs/api-reference/olmv1-api-reference.md index 84fdbfa646..c0da40c4bc 100644 --- a/docs/api-reference/operator-controller-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -239,6 +239,40 @@ _Appears in:_ | `status` _[ClusterExtensionStatus](#clusterextensionstatus)_ | status is an optional field that defines the observed state of the ClusterExtension. | | | +#### ClusterExtensionConfig + + + +ClusterExtensionConfig is a discriminated union which selects the source configuration values to be merged into +the ClusterExtension's rendered manifests. + + + +_Appears in:_ +- [ClusterExtensionSpec](#clusterextensionspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `configType` _[ClusterExtensionConfigType](#clusterextensionconfigtype)_ | configType is a required reference to the type of configuration source.

Allowed values are "Inline"

When this field is set to "Inline", the cluster extension configuration is defined inline within the
ClusterExtension resource. | | Enum: [Inline]
Required: \{\}
| +| `inline` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#json-v1-apiextensions-k8s-io)_ | inline contains JSON or YAML values specified directly in the
ClusterExtension.

inline must be set if configType is 'Inline'.
inline accepts arbitrary JSON/YAML objects.
inline is validation at runtime against the schema provided by the bundle if a schema is provided. | | Type: object
| + + +#### ClusterExtensionConfigType + +_Underlying type:_ _string_ + + + + + +_Appears in:_ +- [ClusterExtensionConfig](#clusterextensionconfig) + +| Field | Description | +| --- | --- | +| `Inline` | | + + #### ClusterExtensionInstallConfig @@ -309,6 +343,7 @@ _Appears in:_ | `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a reference to a ServiceAccount used to perform all interactions
with the cluster that are required to manage the extension.
The ServiceAccount must be configured with the necessary permissions to perform these interactions.
The ServiceAccount must exist in the namespace referenced in the spec.
serviceAccount is required. | | Required: \{\}
| | `source` _[SourceConfig](#sourceconfig)_ | source is a required field which selects the installation source of content
for this ClusterExtension. Selection is performed by setting the sourceType.

Catalog is currently the only implemented sourceType, and setting the
sourcetype to "Catalog" requires the catalog field to also be defined.

Below is a minimal example of a source definition (in yaml):

source:
sourceType: Catalog
catalog:
packageName: example-package | | Required: \{\}
| | `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is an optional field used to configure the installation options
for the ClusterExtension such as the pre-flight check configuration. | | | +| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is an optional field used to specify bundle specific configuration
used to configure the bundle. Configuration is bundle specific and a bundle may provide
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.

config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
to configure the bundle should be found in its end-user documentation. | | | #### ClusterExtensionStatus @@ -328,6 +363,8 @@ _Appears in:_ | `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | | + + #### ImageSource diff --git a/docs/draft/OWNERS b/docs/draft/OWNERS new file mode 100644 index 0000000000..c81ed41103 --- /dev/null +++ b/docs/draft/OWNERS @@ -0,0 +1,2 @@ +approvers: + - docs-draft-approvers diff --git a/docs/draft/api-reference/catalogd-webserver-metas-endpoint.md b/docs/draft/api-reference/catalogd-webserver-metas-endpoint.md index 6b27ba27e2..eb70149dac 100644 --- a/docs/draft/api-reference/catalogd-webserver-metas-endpoint.md +++ b/docs/draft/api-reference/catalogd-webserver-metas-endpoint.md @@ -31,7 +31,7 @@ As an example, to access only the [package schema](https://olm.operatorframework the URL to access the service would be `https://catalogd-service.olmv1-system.svc/catalogs/operatorhubio/api/v1/metas?schema=olm.package` -For more examples of valid queries that can be made to the `api/v1/metas` service endpoint, please see [Catalog Queries](../howto/catalog-queries.md). +For more examples of valid queries that can be made to the `api/v1/metas` service endpoint, please see [Catalog Queries](../../howto/catalog-queries.md). !!! note diff --git a/docs/draft/api-reference/network-policies.md b/docs/draft/api-reference/network-policies.md index 9f36eaae1b..016825ebfd 100644 --- a/docs/draft/api-reference/network-policies.md +++ b/docs/draft/api-reference/network-policies.md @@ -4,8 +4,8 @@ OLMv1 uses [Kubernetes NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/) to secure communication between components, restricting network traffic to only what's necessary for proper functionality. -* The catalogd NetworkPolicy is implemented [here](https://github.com/operator-framework/operator-controller/blob/main/config/base/catalogd/manager/network_policy.yaml). -* The operator-controller is implemented [here](https://github.com/operator-framework/operator-controller/blob/main/config/base/operator-controller/manager/network_policy.yaml). +* The catalogd NetworkPolicy is implemented [here](https://github.com/operator-framework/operator-controller/blob/main/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml). +* The operator-controller is implemented [here](https://github.com/operator-framework/operator-controller/blob/main/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml). This document explains the details of `NetworkPolicy` implementation for the core components. @@ -19,8 +19,8 @@ NetworkPolicy is implemented for both catalogd and operator-controller component Each component has a dedicated NetworkPolicy that applies to its respective pod through label selectors: -* For catalogd: `control-plane=catalogd-controller-manager` -* For operator-controller: `control-plane=operator-controller-controller-manager` +* For catalogd: `app.kubernetes.io/name=catalogd` +* For operator-controller: `app.kubernetes.io/name=operator-controller` ### Catalogd NetworkPolicy @@ -78,10 +78,10 @@ If you encounter network connectivity issues after deploying OLMv1, consider the ```bash # Verify catalogd pod labels -kubectl get pods -n olmv1-system --selector=control-plane=catalogd-controller-manager +kubectl get pods -n olmv1-system --selector=apps.kubernetes.io/name=catalogd # Verify operator-controller pod labels -kubectl get pods -n olmv1-system --selector=control-plane=operator-controller-controller-manager +kubectl get pods -n olmv1-system --selector=apps.kubernetes.io/name=operator-controller # Compare with actual pod names kubectl get pods -n olmv1-system | grep -E 'catalogd|operator-controller' diff --git a/docs/draft/howto/catalog-queries-metas-endpoint.md b/docs/draft/howto/catalog-queries-metas-endpoint.md index f723d504b6..25b45a7a41 100644 --- a/docs/draft/howto/catalog-queries-metas-endpoint.md +++ b/docs/draft/howto/catalog-queries-metas-endpoint.md @@ -1,6 +1,6 @@ # Catalog queries -After you [add a catalog of extensions](../tutorials/add-catalog.md) to your cluster, you must port forward your catalog as a service. +After you [add a catalog of extensions](../../tutorials/add-catalog.md) to your cluster, you must port forward your catalog as a service. Then you can query the catalog by using `curl` commands and the `jq` CLI tool to find extensions to install. ## Prerequisites diff --git a/docs/draft/howto/consuming-metrics.md b/docs/draft/howto/consuming-metrics.md index d896d8081d..ccefbee6c2 100644 --- a/docs/draft/howto/consuming-metrics.md +++ b/docs/draft/howto/consuming-metrics.md @@ -6,7 +6,7 @@ The following procedure is provided as an example for testing purposes. Do not d In OLM v1, you can use the provided metrics with tools such as the [Prometheus Operator][prometheus-operator]. By default, Operator Controller and catalogd export metrics to the `/metrics` endpoint of each service. -You must grant the necessary permissions to access the metrics by using [role-based access control (RBAC) polices][rbac-k8s-docs]. +You must grant the necessary permissions to access the metrics by using [role-based access control (RBAC) polices][rbac-k8s-docs]. You will also need to create a `NetworkPolicy` to allow egress traffic from your scraper pod, as the OLM namespace by default allows only `catalogd` and `operator-controller` to send and receive traffic. Because the metrics are exposed over HTTPS by default, you need valid certificates to use the metrics with services such as Prometheus. The following sections cover enabling metrics, validating access, and provide a reference of a `ServiceMonitor` to illustrate how you might integrate the metrics with the [Prometheus Operator][prometheus-operator] or other third-part solutions. @@ -23,6 +23,25 @@ kubectl create clusterrolebinding operator-controller-metrics-binding \ --serviceaccount=olmv1-system:operator-controller-controller-manager ``` +2. Next, create a `NetworkPolicy` to allow the scraper pods to send their scrape requests: + +```shell +kubectl apply -f - << EOF +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: scraper-policy + namespace: olmv1-system +spec: + podSelector: + matchLabels: + metrics: scraper + policyTypes: + - Egress + egress: + - {} # Allows all egress traffic for metrics requests +EOF +``` ### Validating Access Manually 1. Generate a token for the service account and extract the required certificates: @@ -41,6 +60,8 @@ kind: Pod metadata: name: curl-metrics namespace: olmv1-system + labels: + metrics: scraper spec: serviceAccountName: operator-controller-controller-manager containers: @@ -69,28 +90,27 @@ spec: secretName: olmv1-cert securityContext: runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault restartPolicy: Never EOF ``` -3. Access the pod: +3. Run the following command using the `TOKEN` value obtained above to check the metrics: ```shell -kubectl exec -it curl-metrics -n olmv1-system -- sh -``` - -4. Run the following command using the `TOKEN` value obtained above to check the metrics: - -```shell -curl -v -k -H "Authorization: Bearer " \ +kubectl exec -it curl-metrics -n olmv1-system -- \ +curl -v -k -H "Authorization: Bearer ${TOKEN}" \ https://operator-controller-service.olmv1-system.svc.cluster.local:8443/metrics ``` -5. Run the following command to validate the certificates and token: +4. Run the following command to validate the certificates and token: ```shell +kubectl exec -it curl-metrics -n olmv1-system -- \ curl -v --cacert /tmp/cert/ca.crt --cert /tmp/cert/tls.crt --key /tmp/cert/tls.key \ --H "Authorization: Bearer " \ +-H "Authorization: Bearer ${TOKEN}" \ https://operator-controller-service.olmv1-system.svc.cluster.local:8443/metrics ``` @@ -131,6 +151,8 @@ kind: Pod metadata: name: curl-metrics-catalogd namespace: olmv1-system + labels: + metrics: scraper spec: serviceAccountName: catalogd-controller-manager containers: @@ -159,27 +181,26 @@ spec: secretName: $OLM_SECRET securityContext: runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault restartPolicy: Never EOF ``` -4. Access the pod: - -```shell -kubectl exec -it curl-metrics-catalogd -n olmv1-system -- sh -``` - -5. Run the following command using the `TOKEN` value obtained above to check the metrics: +4. Run the following command using the `TOKEN` value obtained above to check the metrics: ```shell -curl -v -k -H "Authorization: Bearer " \ +kubectl exec -it curl-metrics -n olmv1-system -- \ +curl -v -k -H "Authorization: Bearer ${TOKEN}" \ https://catalogd-service.olmv1-system.svc.cluster.local:7443/metrics ``` -6. Run the following command to validate the certificates and token: +5. Run the following command to validate the certificates and token: ```shell +kubectl exec -it curl-metrics -n olmv1-system -- \ curl -v --cacert /tmp/cert/ca.crt --cert /tmp/cert/tls.crt --key /tmp/cert/tls.key \ --H "Authorization: Bearer " \ +-H "Authorization: Bearer ${TOKEN}" \ https://catalogd-service.olmv1-system.svc.cluster.local:7443/metrics ``` @@ -205,7 +226,7 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: - control-plane: operator-controller-controller-manager + apps.kubernetes.io/name: operator-controller name: controller-manager-metrics-monitor namespace: olmv1-system spec: @@ -230,7 +251,7 @@ spec: key: tls.key selector: matchLabels: - control-plane: operator-controller-controller-manager + apps.kubernetes.io/name: operator-controller EOF ``` @@ -247,13 +268,13 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: - control-plane: catalogd-controller-manager + apps.kubernetes.io/name: catalogd name: catalogd-metrics-monitor namespace: olmv1-system spec: endpoints: - path: /metrics - port: https + port: metrics scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: @@ -272,9 +293,9 @@ spec: key: tls.key selector: matchLabels: - control-plane: catalogd-controller-manager + app.kubernetes.io/name: catalogd EOF ``` [prometheus-operator]: https://github.com/prometheus-operator/kube-prometheus -[rbac-k8s-docs]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ \ No newline at end of file +[rbac-k8s-docs]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ diff --git a/docs/draft/howto/enable-helm-chart-support.md b/docs/draft/howto/enable-helm-chart-support.md new file mode 100644 index 0000000000..44d083707c --- /dev/null +++ b/docs/draft/howto/enable-helm-chart-support.md @@ -0,0 +1,415 @@ +# How to Enable Helm Chart Support Feature Gate + +## Description + +This document outlines the steps to enable the Helm Chart support feature gate in the OLMv1 and subsequently deploy a Helm Chart to a Kubernetes cluster. It involves patching the `operator-controller-controller-manager` deployment to enable the `HelmChartSupport` feature, setting up a network policy for the registry, deploying an OCI registry, and finally creating a ClusterExtension to deploy the metrics server helm chart. + +The feature allows developers and end-users to deploy Helm charts from OCI registries through the `ClusterExtension` API. + +## Demos + +[![Helm Chart Support Demo](https://asciinema.org/a/wEzsqXLDAflJvzSX7QP47GvLw.svg)](https://asciinema.org/a/wEzsqXLDAflJvzSX7QP47GvLw) + + +## Enabling the Feature Gate + +To enable the Helm Chart support feature gate, you need to patch the `operator-controller-controller-manager` deployment in the `olmv1-system` namespace. This will add the `--feature-gates=HelmChartSupport=true` argument to the manager container. + +1. **Create a patch file:** + + ```bash + $ kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=HelmChartSupport=true"}]' + ``` + +2. **Wait for the controller manager pods to be ready:** + + ```bash + $ kubectl -n olmv1-system wait --for condition=ready pods -l apps.kubernetes.io/name=operator-controller + ``` + +Once the above wait condition is met, the `HelmChartSupport` feature gate should be enabled in operator controller. + +## Deploy an OCI Chart registry for testing + +With the operator-controller pod running with the `HelmChartSupport` feature gate enabled, you would need access to a Helm charts +hosted in an OCI registry. For this demo, the instructions will walk you through steps to deploy a registry in the `olmv1-system` +project. + +In addition to the OCI registry, you will need a ClusterCatalog in the Kubernetes cluster which will reference Helm charts in the OCI registry. + +1. **Configure network policy for the registry:** + + ```bash + $ cat << EOF | kubectl -n olmv1-system apply -f - + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: registry + spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + app: registry + policyTypes: + - Ingress + - Egress + EOF + ``` + +2. **Create certificates for the OCI registry:** + + ```bash + $ cat << EOF | kubectl -n olmv1-system apply -f - + --- + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: registry-cert + namespace: olmv1-system + spec: + dnsNames: + - registry.olmv1-system.svc + - registry.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + secretName: registry-cert + status: {} + EOF + ``` + +3. **Deploy an OCI registry:** + + ```bash + $ cat << EOF | kubectl -n olmv1-system apply -f - + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + creationTimestamp: null + labels: + app: registry + name: registry + spec: + replicas: 1 + selector: + matchLabels: + app: registry + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: registry + spec: + containers: + - name: registry + image: docker.io/library/registry:3.0.0 + env: + - name: REGISTRY_HTTP_ADDR + value: "0.0.0.0:8443" + - name: REGISTRY_HTTP_TLS_CERTIFICATE + value: "/certs/tls.crt" + - name: REGISTRY_HTTP_TLS_KEY + value: "/certs/tls.key" + - name: OTEL_TRACES_EXPORTER + value: "none" + ports: + - name: registry + protocol: TCP + containerPort: 8443 + securityContext: + runAsUser: 999 + allowPrivilegeEscalation: false + runAsNonRoot: true + seccompProfile: + type: "RuntimeDefault" + capabilities: + drop: + - ALL + volumeMounts: + - name: blobs + mountPath: /var/lib/registry/docker + - name: certs + mountPath: /certs + resources: {} + volumes: + - name: blobs + emptyDir: {} + - name: certs + secret: + secretName: registry-cert + status: {} + EOF + ``` + +4. **Expose the registry container:** + + ```bash + $ cat << EOF | kubectl -n olmv1-system apply -f - + --- + apiVersion: v1 + kind: Service + metadata: + creationTimestamp: null + labels: + app: registry + name: registry + namespace: olmv1-system + spec: + ports: + - port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: registry + status: + loadBalancer: {} + EOF + ``` + +5. **Wait for the registry pod to be in a Running phase:** + + ```bash + $ kubectl -n olmv1-system wait --for=jsonpath='{.status.phase}'=Running pod -l app=registry + ``` + +6. **Deploy the cluster catalog:** + + ```bash + $ cat << EOF | kubectl apply -f - + --- + apiVersion: olm.operatorframework.io/v1 + kind: ClusterCatalog + metadata: + name: metrics-server-operators + namespace: olmv1-system + spec: + priority: -100 + source: + image: + pollIntervalMinutes: 5 + ref: quay.io/eochieng/metrics-server-catalog:latest + type: Image + EOF + ``` + +7. **Upload charts to the registry:** + + ```bash + $ cat << EOF | kubectl apply -f - + --- + apiVersion: batch/v1 + kind: Job + metadata: + creationTimestamp: null + name: chart-uploader + spec: + template: + metadata: + creationTimestamp: null + spec: + containers: + - image: quay.io/eochieng/uploader:latest + name: chart-uploader + resources: {} + restartPolicy: Never + status: {} + EOF + ``` + +8. **Deploy metrics server RBAC and metrics server:** + + ```bash + $ cat << EOF | kubectl apply -f - + --- + apiVersion: v1 + kind: Namespace + metadata: + creationTimestamp: null + name: metrics-server-system + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + creationTimestamp: null + name: metrics-server-installer + namespace: metrics-server-system + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + creationTimestamp: null + name: metrics-server-crb + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: metrics-server-cr + subjects: + - kind: ServiceAccount + name: metrics-server-installer + namespace: metrics-server-system + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + creationTimestamp: null + name: metrics-server-cr + rules: + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - list + - watch + - get + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + - clusterrolebindings + - rolebindings + verbs: + - create + - delete + - list + - watch + - get + - patch + - update + - apiGroups: + - "" + resources: + - services + - secrets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - apps + resources: + - deployments + - deployments/finalizers + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - apiregistration.k8s.io + resources: + - apiservices + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + - clusterextensions/finalizers + verbs: + - get + - list + - watch + - create + - delete + - update + - patch + - apiGroups: + - metrics.k8s.io + resources: + - nodes + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + - namespaces + - nodes + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - nodes/metrics + verbs: + - get + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + EOF + ``` + +9. **Deploy metrics server cluster extension:** + + ```bash + $ cat << EOF | kubectl apply -f - + --- + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: metrics-server + namespace: metrics-server-system + spec: + namespace: metrics-server-system + serviceAccount: + name: metrics-server-installer + source: + sourceType: Catalog + catalog: + packageName: metrics-server + version: 3.12.0 + EOF + ``` + +10. **Confirm the Helm chart has been deployed:** + + ```bash + $ kubectl get clusterextensions metrics-server + NAME INSTALLED BUNDLE VERSION INSTALLED PROGRESSING AGE + metrics-server metrics-server.v3.12.0 3.12.0 True True 4m40s + ``` diff --git a/docs/draft/howto/enable-webhook-support.md b/docs/draft/howto/enable-webhook-support.md new file mode 100644 index 0000000000..f5c7de7767 --- /dev/null +++ b/docs/draft/howto/enable-webhook-support.md @@ -0,0 +1,55 @@ +## Installation of Bundles containing Webhooks + +!!! note +OLMv1 supports the installation of bundles containing webhooks by default. +By default, OLM v1 uses the community Cert Manager package for admission webhook via the feature-gate flag `WebhookProviderCertManager`. To use the OpenShift Service CA provider, set the `--feature-gates=WebhookProviderOpenshiftServiceCA=true` flag at startup. + +Admission webhooks are part of the Kubernetes suite of [Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) +plugins. Webhooks run as services called by the kube-apiservice in due course of processing a resource related request. They can be used to validate resources, ensure reasonable default values, +are set, or aid in the migration to new CustomResourceDefinition schema. The communication with the webhook service is secured by TLS. In OLMv1, the TLS certificate is managed by a +certificate provider. Currently, two certificate providers are supported: CertManager and Openshift-ServiceCA. The certificate provider to use given by the feature-gate: + +- `WebhookProviderCertManager` for [CertManager](https://cert-manager.io/) +- `WebhookProviderOpenshiftServiceCA` for [Openshift-ServiceCA](https://github.com/openshift/service-ca-operator) + +As CertManager is already installed with OLMv1, we suggest using `WebhookProviderCertManager`. + +### Run OLM v1 with Webhook Support + +```terminal title=Start the controller with webhook support +make run +``` + +Then, + +```terminal title=Wait for rollout to complete +kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager +``` + +### Notes on the generated certificate + +#### CertManager + +The generated certificate maintains a high-level of parity with the certificate generated by OLMv0: +- Self-signed +- Two validity period, rotating 24h before expiry +- Valid for the webhook service's DNSNames: + - . + - ..svc + - ..svc.cluster.local + +#### Openshift-ServiceCA + +Generation and rotation are completely governed by [Openshift-ServiceCA](https://github.com/openshift/service-ca-operator) + +### How does it work? + +There's no change in the installation flow. Just install a bundle containing webhooks as you would any other. + +### Demo + +!!! note +As there is no difference in usage or experience between the CertManager and Openshift-ServiceCA variants, only +the cert-manager variant is demoed. + +[![asciicast](https://asciinema.org/a/GyjsB129GkUadeuxFhNuG4FcS.svg)](https://asciinema.org/a/GyjsB129GkUadeuxFhNuG4FcS) diff --git a/docs/draft/howto/profiling_with_pprof.md b/docs/draft/howto/profiling_with_pprof.md index 23ec7f7af9..01c0969d48 100644 --- a/docs/draft/howto/profiling_with_pprof.md +++ b/docs/draft/howto/profiling_with_pprof.md @@ -21,7 +21,7 @@ The following steps are examples to demonstrate the required changes to enable P 1. Run the following command to patch the Deployment and add the `--pprof-bind-address=:8082` flag: ```shell -kubectl patch deployment $(kubectl get deployments -n olmv1-system -l control-plane=operator-controller-controller-manager -o jsonpath='{.items[0].metadata.name}') \ +kubectl patch deployment $(kubectl get deployments -n olmv1-system -l apps.kubernetes.io/name=operator-controller -o jsonpath='{.items[0].metadata.name}') \ -n olmv1-system --type='json' -p='[ { "op": "add", @@ -127,7 +127,7 @@ go tool pprof -http=:8080 ./operator-controller-profile.pprof 1. Run the following command to patch the Deployment and add the `--pprof-bind-address=:8083` flag: ```shell -kubectl patch deployment $(kubectl get deployments -n olmv1-system -l control-plane=catalogd-controller-manager -o jsonpath='{.items[0].metadata.name}') \ +kubectl patch deployment $(kubectl get deployments -n olmv1-system -l apps.kubernetes.io/name=catalogd -o jsonpath='{.items[0].metadata.name}') \ -n olmv1-system --type='json' -p='[ { "op": "add", @@ -235,7 +235,7 @@ go tool pprof -http=:8080 ./catalogd-profile.pprof 1. Run the following command to bind to `--pprof-bind-address` the value `0` in order to disable the endpoint. ```shell -kubectl patch deployment $(kubectl get deployments -n olmv1-system -l control-plane=operator-controller-controller-manager -o jsonpath='{.items[0].metadata.name}') \ +kubectl patch deployment $(kubectl get deployments -n olmv1-system -l apps.kubernetes.io/name=operator-controller -o jsonpath='{.items[0].metadata.name}') \ -n olmv1-system --type='json' -p='[ { "op": "replace", @@ -266,7 +266,7 @@ kubectl delete pod curl-oper-con-pprof -n olmv1-system 1. Run the following command to bind to `--pprof-bind-address` the value `0` in order to disable the endpoint. ```shell -kubectl patch deployment $(kubectl get deployments -n olmv1-system -l control-plane=catalogd-controller-manager -o jsonpath='{.items[0].metadata.name}') \ +kubectl patch deployment $(kubectl get deployments -n olmv1-system -l apps.kubernetes.io/name=catalogd -o jsonpath='{.items[0].metadata.name}') \ -n olmv1-system --type='json' -p='[ { "op": "replace", @@ -294,4 +294,4 @@ re-start the deployment `kubectl rollout restart deployment -n olmv1-system cata kubectl delete pod curl-catalogd-pprof -n olmv1-system ``` -[pprof]: https://github.com/google/pprof/blob/main/doc/README.md \ No newline at end of file +[pprof]: https://github.com/google/pprof/blob/main/doc/README.md diff --git a/docs/draft/howto/rbac-permissions-checking.md b/docs/draft/howto/rbac-permissions-checking.md new file mode 100644 index 0000000000..04c13bf0ce --- /dev/null +++ b/docs/draft/howto/rbac-permissions-checking.md @@ -0,0 +1,54 @@ +# How To Get Your Cluster Extension RBAC Right — Working with the Preflight Permissions Check + +Cluster Extensions in Operator Lifecycle Manager (OLM) v1 are installed and managed via a **service account** that you (the cluster admin) provide. Unlike OLM v0, OLM v1 itself doesn’t have cluster-admin privileges to grant Operators the access they need – **you** must ensure the service account has all necessary Role-Based Access Control (RBAC) permissions. If the service account is missing permissions, the extension’s installation will fail or hang. To address this, the **operator-controller** now performs a **preflight permissions check** before installing an extension. This check identifies any missing RBAC permissions up front and surfaces them to you so that you can fix the issues. + +## Understanding the Preflight Permissions Check + +When you create a `ClusterExtension` Custom Resource (CR) to install an Operator extension, the operator-controller will do a dry-run of the installation and verify that the specified service account can perform all the actions required by that extension. This includes creating all the Kubernetes objects in the bundle (Deployments, Services, CRDs, etc.), as well as creating any RBAC roles or bindings that the extension’s bundle defines. + +If any required permission is missing, the preflight check will **fail fast** *before* attempting the real installation. Instead of proceeding, the operator-controller records which permissions are missing. You’ll find this information in two places: + +- **ClusterExtension Status Conditions:** The `ClusterExtension` CR will have a condition (such as **Progressing** or **Installing**) with a message describing the missing permissions. The condition’s reason may be set to “Retrying” (meaning the controller will periodically retry the install) and the message will start with “pre-authorization failed: …”. +- **Operator-Controller Logs:** The same message is also logged by the operator-controller pod. If you have access to the operator-controller’s logs (in namespace `olm-controller` on OpenShift), you can see the detailed RBAC errors there as well. + +### Interpreting the Preflight Check Output + +The preflight check’s output enumerates the **RBAC rules** that the service account is missing. Each missing permission is listed in a structured format. For example, a message might say: + +``` +service account requires the following permissions to manage cluster extension: + Namespace:"" APIGroups:[] Resources:[services] Verbs:[list,watch] + Namespace:"pipelines" APIGroups:[] Resources:[secrets] Verbs:[get] +``` + +Let’s break down how to read this output: + +- **`Namespace:""`** – An empty namespace in quotes means the permission is needed at the **cluster scope** (not limited to a single namespace). In the example above, `Namespace:""` for Services indicates the service account needs the ability to list/watch Services cluster-wide. +- **`APIGroups:[]`** – An empty API group (`[]`) means the **core API group** (no group). For instance, core resources like Services, Secrets, ConfigMaps have `APIGroups:[]`. If the resource is part of a named API group (e.g. `apps`, `apiextensions.k8s.io`), that group would be listed here. +- **`Resources:[...]`** – The resource type that’s missing permissions. e.g. `services`, `secrets`, `customresourcedefinitions`. +- **`Verbs:[...]`** – The specific actions (verbs) that the service account is not allowed to do for that resource. Multiple verbs listed together means none of those verbs are permitted (and are all required). + +A few special cases to note: + +- **Privilege Escalation Cases:** If the extension’s bundle includes the creation of a Role or ClusterRole, the service account needs to have at least the permissions it is trying to grant. If not, the preflight check will report those verbs as missing to prevent privilege escalation. +- **Missing Role References (Resolution Errors):** If an Operator’s bundle references an existing ClusterRole or Role that is not found, the preflight check will report an “authorization evaluation error” listing the missing role. + +## Resolving Common RBAC Permission Errors + +Once you understand what each missing permission is, the fix is usually straightforward: **grant the service account those permissions**. Here are common scenarios and how to address them: + +- **Missing resource permissions (verbs):** Update or create a (Cluster)Role and RoleBinding/ClusterRoleBinding to grant the missing verbs on the resources in the specified namespaces or at cluster scope. +- **Privilege escalation missing permissions:** Treat these missing verbs as required for the installer as well, granting the service account those rights so it can create the roles it needs. +- **Missing roles/clusterroles:** Ensure any referenced roles exist by creating them or adjusting the extension’s expectations. + +## Demo Scenario (OpenShift) + +Below is an example demo you can run on OpenShift to see the preflight check in action: + +1. **Create a minimal ServiceAccount and ClusterRole** that lacks key permissions (e.g., missing list/watch on Services and create on CRDs). +2. **Apply a ClusterExtension** pointing to the Pipelines Operator package, specifying the above service account. +3. **Describe the ClusterExtension** (`oc describe clusterextension pipelines-operator`) to see the preflight “pre-authorization failed” errors listing missing permissions. +4. **Update the ClusterRole** to include the missing verbs. +5. **Reapply the role** and observe the ClusterExtension status clear and the operator proceed with installation. + +By following this guide and using the preflight output, you can iteratively grant exactly the permissions needed—no more, no less—ensuring your cluster extensions install reliably and securely. diff --git a/docs/draft/howto/single-ownnamespace-install.md b/docs/draft/howto/single-ownnamespace-install.md index 2f440f32d2..4152946871 100644 --- a/docs/draft/howto/single-ownnamespace-install.md +++ b/docs/draft/howto/single-ownnamespace-install.md @@ -1,8 +1,7 @@ ## Description !!! note -This feature is still in *alpha* the `SingleOwnNamespaceInstallSupport` feature-gate must be enabled to make use of it. -See the instructions below on how to enable it. +The `SingleOwnNamespaceInstallSupport` feature-gate is enabled by default. Use this guide to configure bundles that need Single or Own namespace install modes. --- @@ -31,36 +30,46 @@ include *installModes*. [![OwnNamespace Install Demo](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i.svg)](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i) -## Enabling the Feature-Gate - -!!! tip +## Configuring the `ClusterExtension` -This guide assumes OLMv1 is already installed. If that is not the case, -you can follow the [getting started](../../getting-started/olmv1_getting_started.md) guide to install OLMv1. +A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the +`.spec.config.inline.watchNamespace` property which may or may not be present or required depending on the bundle's +install mode support, if the bundle: ---- + - only supports *AllNamespaces* mode => `watchNamespace` is not a configuration + - supports *AllNamespaces* and *SingleNamespace* and/or *OwnNamespace* => `watchNamespace` is optional + - bundle only supports *SingleNamespace* and/or *OwnNamespace* => `watchNamespace` is required -Patch the `operator-controller` `Deployment` adding `--feature-gates=SingleOwnNamespaceInstallSupport=true` to the -controller container arguments: +The `watchNamespace` configuration can only be the install namespace if the bundle supports the *OwnNamespace* install mode, and +it can only be any other namespace if the bundle supports the *SingleNamespace* install mode. -```terminal title="Enable SingleOwnNamespaceInstallSupport feature-gate" -kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' -``` +Examples: -Wait for `Deployment` rollout: +Bundle only supports *AllNamespaces*: +- `watchNamespace` is not a configuration +- bundle will be installed in *AllNamespaces* mode -```terminal title="Wait for Deployment rollout" -kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager -``` +Bundle only supports *OwnNamespace*: +- `watchNamespace` is required +- `watchNamespace` must be the install namespace +- bundle will always be installed in *OwnNamespace* mode -## Configuring the `ClusterExtension` +Bundle supports *AllNamespace* and *OwnNamespace*: +- `watchNamespace` is optional +- if `watchNamespace` = install namespace => bundle will be installed in *OwnNamespace* mode +- if `watchNamespace` is null or not set => bundle will be installed in *AllNamespaces* mode +- if `watchNamespace` != install namespace => error -A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the -`olm.operatorframework.io/watch-namespace: ` annotation. The *installMode* is inferred in the following way: +Bundle only supports *SingleNamespace*: +- `watchNamespace` is required +- `watchNamespace` must *NOT* be the install namespace +- bundle will always be installed in *SingleNamespace* mode - - *AllNamespaces*: `` is empty, or the annotation is not present - - *OwnNamespace*: `` is the install namespace (i.e. `.spec.namespace`) - - *SingleNamespace*: `` not the install namespace +Bundle supports *AllNamespaces*, *SingleNamespace*, and *OwnNamespace* install modes: +- `watchNamespace` can be optionally configured +- if `watchNamespace` = install namespace => bundle will be installed in *OwnNamespace* mode +- if `watchNamespace` != install namespace => bundle will be installed in *SingleNamespace* mode +- if `watchNamespace` is null or not set => bundle will be installed in *AllNamespaces* mode ### Examples @@ -70,12 +79,13 @@ apiVersion: olm.operatorframework.io/v1 kind: ClusterExtension metadata: name: argocd - annotations: - olm.operatorframework.io/watch-namespace: argocd-watch spec: namespace: argocd serviceAccount: name: argocd-installer + config: + inline: + watchNamespace: argocd-watch source: sourceType: Catalog catalog: @@ -90,12 +100,13 @@ apiVersion: olm.operatorframework.io/v1 kind: ClusterExtension metadata: name: argocd - annotations: - olm.operatorframework.io/watch-namespace: argocd spec: namespace: argocd serviceAccount: name: argocd-installer + config: + inline: + watchNamespace: argocd source: sourceType: Catalog catalog: diff --git a/docs/draft/howto/use-synthetic-permissions.md b/docs/draft/howto/use-synthetic-permissions.md index 15f9c2c207..9820ad2b6e 100644 --- a/docs/draft/howto/use-synthetic-permissions.md +++ b/docs/draft/howto/use-synthetic-permissions.md @@ -8,10 +8,10 @@ Synthetic user permissions enables fine-grained configuration of ClusterExtensio User can not only configure RBAC permissions governing the management across all ClusterExtensions, but also on a case-by-case basis. -### Update OLM to enable Feature +### Run OLM v1with Experimental Features Enabled -```terminal title=Enable SyntheticPermissions feature -kubectl kustomize config/overlays/featuregate/synthetic-user-permissions | kubectl apply -f - +```terminal title=Enable Experimental Features in a New Kind Cluster +make run-experimental ``` ```terminal title=Wait for rollout to complete diff --git a/docs/draft/tutorials/explore-available-content-metas-endpoint.md b/docs/draft/tutorials/explore-available-content-metas-endpoint.md index 8ece0a75d8..5d04b02df1 100644 --- a/docs/draft/tutorials/explore-available-content-metas-endpoint.md +++ b/docs/draft/tutorials/explore-available-content-metas-endpoint.md @@ -5,7 +5,7 @@ hide: # Explore Available Content -After you [add a catalog of extensions](add-catalog.md) to your cluster, you must port forward your catalog as a service. +After you [add a catalog of extensions](../../tutorials/add-catalog.md) to your cluster, you must port forward your catalog as a service. Then you can query the catalog by using `curl` commands and the `jq` CLI tool to find extensions to install. ## Prerequisites @@ -91,9 +91,6 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ... ``` - !!! important - Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces. - 3. Return list of packages which support `AllNamespaces` install mode, do not use webhooks, and where the channel head version uses `olm.csv.metadata` format: ``` terminal @@ -144,4 +141,4 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ### Additional resources -* [Catalog queries](../howto/catalog-queries.md) +* [Catalog queries](../../howto/catalog-queries.md) diff --git a/docs/project/olmv1_limitations.md b/docs/project/olmv1_limitations.md index 26e2340ff5..54e174b4ca 100644 --- a/docs/project/olmv1_limitations.md +++ b/docs/project/olmv1_limitations.md @@ -8,8 +8,7 @@ hide: Currently, OLM v1 only supports installing operators packaged in [OLM v0 bundles](https://olm.operatorframework.io/docs/tasks/creating-operator-bundle/) , also known as `registry+v1` bundles. Additionally, the bundled operator, or cluster extension: -* **must** support installation via the `AllNamespaces` install mode. -* **must not** use webhooks. +* **must** support installation via the `AllNamespaces`, `SingleNamespace`, or `OwnNamespace` install modes. * **must not** declare dependencies using any of the following file-based catalog properties: * `olm.gvk.required` * `olm.package.required` diff --git a/docs/project/public-api.md b/docs/project/public-api.md index a0e45ce93d..687c14a2a5 100644 --- a/docs/project/public-api.md +++ b/docs/project/public-api.md @@ -2,8 +2,7 @@ The public API of OLM v1 is as follows: - Kubernetes APIs. For more information on these APIs, see: - - [operator-controller API reference](../api-reference/operator-controller-api-reference.md) - - [catalogd API reference](../api-reference/catalogd-api-reference.md) + - [OLMv1 API reference](../api-reference/olmv1-api-reference.md) - `Catalogd` web server. For more information on what this includes, see the [catalogd web server documentation](../api-reference/catalogd-webserver.md) !!! warning diff --git a/docs/tutorials/explore-available-content.md b/docs/tutorials/explore-available-content.md index 0a1f468093..98bb7733c6 100644 --- a/docs/tutorials/explore-available-content.md +++ b/docs/tutorials/explore-available-content.md @@ -91,9 +91,6 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ... ``` - !!! important - Currently, OLM 1.0 does not support the installation of extensions that use webhooks or that target a single or specified set of namespaces. - 3. Return list of packages that support `AllNamespaces` install mode and do not use webhooks: ``` terminal diff --git a/go.mod b/go.mod index 7190953db1..6da4ac25c1 100644 --- a/go.mod +++ b/go.mod @@ -1,138 +1,142 @@ module github.com/operator-framework/operator-controller -go 1.23.4 +go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 - github.com/Masterminds/semver/v3 v3.3.1 + github.com/Masterminds/semver/v3 v3.4.0 github.com/blang/semver/v4 v4.0.0 - github.com/cert-manager/cert-manager v1.17.1 - github.com/containerd/containerd v1.7.27 - github.com/containers/image/v5 v5.35.0 + github.com/cert-manager/cert-manager v1.18.2 + github.com/containerd/containerd v1.7.28 github.com/fsnotify/fsnotify v1.9.0 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.20.3 + github.com/google/go-containerregistry v0.20.6 + github.com/google/renameio/v2 v2.0.0 github.com/gorilla/handlers v1.5.2 - github.com/klauspost/compress v1.18.0 + github.com/klauspost/compress v1.18.1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 - github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 - github.com/operator-framework/api v0.31.0 + github.com/operator-framework/api v0.35.0 github.com/operator-framework/helm-operator-plugins v0.8.0 - github.com/operator-framework/operator-registry v1.55.0 - github.com/prometheus/client_golang v1.22.0 - github.com/spf13/cobra v1.9.1 - github.com/stretchr/testify v1.10.0 - golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 - golang.org/x/mod v0.24.0 - golang.org/x/sync v0.14.0 - golang.org/x/tools v0.33.0 - gopkg.in/yaml.v2 v2.4.0 - helm.sh/helm/v3 v3.17.3 - k8s.io/api v0.32.3 - k8s.io/apiextensions-apiserver v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/apiserver v0.32.3 - k8s.io/cli-runtime v0.32.3 - k8s.io/client-go v0.32.3 - k8s.io/component-base v0.32.3 + github.com/operator-framework/operator-registry v1.60.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.67.1 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + go.podman.io/image/v5 v5.38.0 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/mod v0.29.0 + golang.org/x/sync v0.17.0 + golang.org/x/tools v0.38.0 + helm.sh/helm/v3 v3.19.0 + k8s.io/api v0.34.1 + k8s.io/apiextensions-apiserver v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/cli-runtime v0.34.0 + k8s.io/client-go v0.34.1 + k8s.io/component-base v0.34.1 k8s.io/klog/v2 v2.130.1 - k8s.io/kubernetes v1.32.3 - k8s.io/utils v0.0.0-20241210054802-24370beab758 - sigs.k8s.io/controller-runtime v0.20.4 - sigs.k8s.io/yaml v1.4.0 + k8s.io/kubernetes v1.34.0 + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + pkg.package-operator.run/boxcutter v0.7.1 + sigs.k8s.io/controller-runtime v0.22.1 + sigs.k8s.io/controller-tools v0.19.0 + sigs.k8s.io/crdify v0.5.0 + sigs.k8s.io/yaml v1.6.0 ) require ( - k8s.io/component-helpers v0.32.3 // indirect - k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + k8s.io/component-helpers v0.34.0 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect ) require ( - cel.dev/expr v0.23.1 // indirect - dario.cat/mergo v1.0.1 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + cel.dev/expr v0.24.0 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.12.9 // indirect + github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect - github.com/containerd/containerd/api v1.8.0 // indirect - github.com/containerd/continuity v0.4.4 // indirect + github.com/containerd/containerd/api v1.9.0 // indirect + github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect - github.com/containers/common v0.63.0 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.1 // indirect - github.com/containers/storage v1.58.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v28.1.1+incompatible // indirect + github.com/docker/cli v28.5.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.0.4+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.1 // indirect - github.com/go-git/go-git/v5 v5.13.1 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.1 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.25.0 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -147,35 +151,33 @@ require ( github.com/klauspost/pgzip v1.2.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect + github.com/letsencrypt/boulder v0.0.0-20250624003606-5ddd5acf990d // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/oklog/ulid v1.3.1 // indirect - github.com/onsi/gomega v1.37.0 // indirect + github.com/onsi/gomega v1.38.2 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/operator-framework/operator-lib v0.17.0 // indirect github.com/otiai10/copy v1.14.1 // indirect @@ -183,136 +185,133 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/proglottis/gpgme v0.1.4 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/proglottis/gpgme v0.1.5 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sigstore/fulcio v1.6.6 // indirect - github.com/sigstore/protobuf-specs v0.4.1 // indirect - github.com/sigstore/rekor v1.3.10 // indirect - github.com/sigstore/sigstore v1.9.3 // indirect + github.com/sigstore/fulcio v1.7.1 // indirect + github.com/sigstore/protobuf-specs v0.4.3 // indirect + github.com/sigstore/sigstore v1.9.5 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/smallstep/pkcs7 v0.1.1 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/spf13/cast v1.7.1 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.1 // indirect - github.com/vbauerster/mpb/v8 v8.9.3 // indirect + github.com/vbauerster/mpb/v8 v8.10.2 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.etcd.io/bbolt v1.4.0 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect + go.etcd.io/bbolt v1.4.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.podman.io/common v0.65.0 // indirect + go.podman.io/storage v1.61.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.13.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/controller-manager v0.32.3 // indirect - k8s.io/kubectl v0.32.3 // indirect - oras.land/oras-go v1.2.5 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 // indirect + k8s.io/controller-manager v0.33.2 // indirect + k8s.io/kubectl v0.34.0 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) -// cel-go v0.23.0 upgrade causes errors raised from the vendor source which lead to think in -// incompatibilities scenarios. After upgrade to use the latest versions of k8s/api v0.33+ -// we should try to see if we could fix this one and remove this replace -replace github.com/google/cel-go => github.com/google/cel-go v0.22.1 +retract v1.5.0 // contains filename with ':' which causes failure creating module zip file -replace k8s.io/api => k8s.io/api v0.32.3 +replace k8s.io/api => k8s.io/api v0.34.0 -replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.3 +replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.34.0 -replace k8s.io/apimachinery => k8s.io/apimachinery v0.32.3 +replace k8s.io/apimachinery => k8s.io/apimachinery v0.34.0 -replace k8s.io/apiserver => k8s.io/apiserver v0.32.3 +replace k8s.io/apiserver => k8s.io/apiserver v0.34.0 -replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.32.3 +replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.34.0 -replace k8s.io/client-go => k8s.io/client-go v0.32.3 +replace k8s.io/client-go => k8s.io/client-go v0.34.0 -replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.32.3 +replace k8s.io/cloud-provider => k8s.io/cloud-provider v0.34.0 -replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.32.3 +replace k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.34.0 -replace k8s.io/code-generator => k8s.io/code-generator v0.32.3 +replace k8s.io/code-generator => k8s.io/code-generator v0.34.0 -replace k8s.io/component-base => k8s.io/component-base v0.32.3 +replace k8s.io/component-base => k8s.io/component-base v0.34.0 -replace k8s.io/component-helpers => k8s.io/component-helpers v0.32.3 +replace k8s.io/component-helpers => k8s.io/component-helpers v0.34.0 -replace k8s.io/controller-manager => k8s.io/controller-manager v0.32.3 +replace k8s.io/controller-manager => k8s.io/controller-manager v0.34.0 -replace k8s.io/cri-api => k8s.io/cri-api v0.32.3 +replace k8s.io/cri-api => k8s.io/cri-api v0.34.0 -replace k8s.io/cri-client => k8s.io/cri-client v0.32.3 +replace k8s.io/cri-client => k8s.io/cri-client v0.34.0 -replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.32.3 +replace k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.34.0 -replace k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.32.3 +replace k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.34.0 -replace k8s.io/endpointslice => k8s.io/endpointslice v0.32.3 +replace k8s.io/endpointslice => k8s.io/endpointslice v0.34.0 -replace k8s.io/externaljwt => k8s.io/externaljwt v0.32.3 +replace k8s.io/externaljwt => k8s.io/externaljwt v0.34.0 -replace k8s.io/kms => k8s.io/kms v0.32.3 +replace k8s.io/kms => k8s.io/kms v0.34.0 -replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.32.3 +replace k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.34.0 -replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.32.3 +replace k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.34.0 -replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.32.3 +replace k8s.io/kube-proxy => k8s.io/kube-proxy v0.34.0 -replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.32.3 +replace k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.34.0 -replace k8s.io/kubectl => k8s.io/kubectl v0.32.3 +replace k8s.io/kubectl => k8s.io/kubectl v0.34.0 -replace k8s.io/kubelet => k8s.io/kubelet v0.32.3 +replace k8s.io/kubelet => k8s.io/kubelet v0.34.0 -replace k8s.io/kubernetes => k8s.io/kubernetes v1.32.3 +replace k8s.io/kubernetes => k8s.io/kubernetes v1.34.0 -replace k8s.io/metrics => k8s.io/metrics v0.32.3 +replace k8s.io/metrics => k8s.io/metrics v0.34.0 -replace k8s.io/mount-utils => k8s.io/mount-utils v0.32.3 +replace k8s.io/mount-utils => k8s.io/mount-utils v0.34.0 -replace k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.32.3 +replace k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.34.0 -replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.32.3 +replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.34.0 diff --git a/go.sum b/go.sum index 812840e25e..8b2845753d 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ -cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= -cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -18,41 +18,37 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= -github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cert-manager/cert-manager v1.17.1 h1:Aig+lWMoLsmpGd9TOlTvO4t0Ah3D+/vGB37x/f+ZKt0= -github.com/cert-manager/cert-manager v1.17.1/go.mod h1:zeG4D+AdzqA7hFMNpYCJgcQ2VOfFNBa+Jzm3kAwiDU4= +github.com/cert-manager/cert-manager v1.18.2 h1:H2P75ycGcTMauV3gvpkDqLdS3RSXonWF2S49QGA1PZE= +github.com/cert-manager/cert-manager v1.18.2/go.mod h1:icDJx4kG9BCNpGjBvrmsFd99d+lXUvWdkkcrSSQdIiw= 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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= @@ -61,12 +57,12 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= -github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= -github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= +github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= +github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -75,26 +71,20 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE= +github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= -github.com/containers/common v0.63.0 h1:ox6vgUYX5TSvt4W+bE36sYBVz/aXMAfRGVAgvknSjBg= -github.com/containers/common v0.63.0/go.mod h1:+3GCotSqNdIqM3sPs152VvW7m5+Mg8Kk+PExT3G9hZw= -github.com/containers/image/v5 v5.35.0 h1:T1OeyWp3GjObt47bchwD9cqiaAm/u4O4R9hIWdrdrP8= -github.com/containers/image/v5 v5.35.0/go.mod h1:8vTsgb+1gKcBL7cnjyNOInhJQfTUQjJoO2WWkKDoebM= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= -github.com/containers/storage v1.58.0 h1:Q7SyyCCjqgT3wYNgRNIL8o/wUS92heIj2/cc8Sewvcc= -github.com/containers/storage v1.58.0/go.mod h1:w7Jl6oG+OpeLGLzlLyOZPkmUso40kjpzgrHUk5tyBlo= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -108,42 +98,42 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0-rc.3 h1:JRJso9IVLoooKX76oWR+DWCCdZlK5m4nRtDWvzB1ITg= -github.com/distribution/distribution/v3 v3.0.0-rc.3/go.mod h1:offoOgrnYs+CFwis8nE0hyzYZqRCZj5EFc5kgfszwiE= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= -github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= +github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= -github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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/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/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 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/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -152,65 +142,72 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= -github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= -github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= -github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= +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.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= +github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -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-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 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/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= -github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= +github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -219,7 +216,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 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.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= @@ -232,29 +228,28 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= -github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/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/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= -github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4= -github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -262,16 +257,16 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc= @@ -281,11 +276,10 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= +github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= +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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -300,19 +294,16 @@ github.com/joelanford/ignore v0.1.1 h1:vKky5RDoPT+WbONrbQBgOn95VV/UPh4ejlyAbbzgn github.com/joelanford/ignore v0.1.1/go.mod h1:8eho/D8fwQ3rIXrLwE23AaeaGDNXqLE9QJ3zJ4LIPCw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/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.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= @@ -323,8 +314,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= -github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= +github.com/letsencrypt/boulder v0.0.0-20250624003606-5ddd5acf990d h1:fCRb9hXR4QQJpwc7xnGugnva0DD5ollTGkys0n8aXT4= +github.com/letsencrypt/boulder v0.0.0-20250624003606-5ddd5acf990d/go.mod h1:BVoSL2Ed8oCncct0meeBqoTY7b1Mzx7WqEOZ8EisFmY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -339,9 +330,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D 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/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= -github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= @@ -352,8 +342,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -364,8 +352,8 @@ github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCnd github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -375,39 +363,39 @@ github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFL 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 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= +github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11 h1:eTNDkNRNV5lZvUbVM9Nop0lBcljSnA8rZX6yQPZ0ZnU= -github.com/openshift/crd-schema-checker v0.0.0-20240404194209-35a9033b1d11/go.mod h1:EmVJt97N+pfWFsli/ipXTBZqSG5F5KGQhm3c3IsGq1o= -github.com/operator-framework/api v0.31.0 h1:tRsFTuZ51xD8U5QgiPo3+mZgVipHZVgRXYrI6RRXOh8= -github.com/operator-framework/api v0.31.0/go.mod h1:57oCiHNeWcxmzu1Se8qlnwEKr/GGXnuHvspIYFCcXmY= +github.com/operator-framework/api v0.35.0 h1:xKrffuGEagk3CWy6zqdK5YmIErlBtWUblNNK+q7ld7c= +github.com/operator-framework/api v0.35.0/go.mod h1:A9UNu/pdcO1RauMHvV54unp4DNm/Y5fMVbGDpnIIF+M= github.com/operator-framework/helm-operator-plugins v0.8.0 h1:0f6HOQC5likkf0b/OvGvw7nhDb6h8Cj5twdCNjwNzMc= github.com/operator-framework/helm-operator-plugins v0.8.0/go.mod h1:Sc+8bE38xTCgCChBUvtq/PxatEg9fAypr7S5iAw8nlA= github.com/operator-framework/operator-lib v0.17.0 h1:cbz51wZ9+GpWR1ZYP4CSKSSBxDlWxmmnseaHVZZjZt4= github.com/operator-framework/operator-lib v0.17.0/go.mod h1:TGopBxIE8L6E/Cojzo26R3NFp1eNlqhQNmzqhOblaLw= -github.com/operator-framework/operator-registry v1.55.0 h1:iXlv53fYyg2VtLqSDEalXD72/5Uzc7Rfx17j35+8plA= -github.com/operator-framework/operator-registry v1.55.0/go.mod h1:8htDRYKWZ6UWjGMXbBdwwHefsJknodOiGLnpjxgAflw= +github.com/operator-framework/operator-registry v1.60.0 h1:eUP14WThVTNx+/5hQR9Jyg0nxbf5cOg7hK/GgaOA5Tg= +github.com/operator-framework/operator-registry v1.60.0/go.mod h1:PojPivJbKZgD9RG77JWxFpQRo3iCoUn6WR3aTiS6HBI= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -416,7 +404,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -424,75 +411,62 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/proglottis/gpgme v0.1.4 h1:3nE7YNA70o2aLjcg63tXMOhPD7bplfE5CBdV+hLAm2M= -github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= -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.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -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/proglottis/gpgme v0.1.5 h1:KCGyOw8sQ+SI96j6G8D8YkOGn+1TwbQTT9/zQXoVlz0= +github.com/proglottis/gpgme v0.1.5/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= +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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -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.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +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.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 h1:uTiEyEyfLhkw678n6EulHVto8AkcXVr8zUcBJNZ0ark= +github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0/go.mod h1:eFYL/99JvdLP4T9/3FZ5t2pClnv7mMskc+WstTcyVr4= +github.com/redis/go-redis/extra/redisotel/v9 v9.10.0 h1:4z7/hCJ9Jft8EBb2tDmK38p2WjyIEJ1ShhhwAhjOCps= +github.com/redis/go-redis/extra/redisotel/v9 v9.10.0/go.mod h1:B0thqLh4hB8MvvcUKSwyP5YiIcCCp8UrQ0cA9gEqyjk= +github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= +github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +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/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= -github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +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/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= +github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= 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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sigstore/fulcio v1.6.6 h1:XaMYX6TNT+8n7Npe8D94nyZ7/ERjEsNGFC+REdi/wzw= -github.com/sigstore/fulcio v1.6.6/go.mod h1:BhQ22lwaebDgIxVBEYOOqLRcN5+xOV+C9bh/GUXRhOk= -github.com/sigstore/protobuf-specs v0.4.1 h1:5SsMqZbdkcO/DNHudaxuCUEjj6x29tS2Xby1BxGU7Zc= -github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU= -github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A= -github.com/sigstore/sigstore v1.9.3 h1:y2qlTj+vh+Or3ictKuR3JUFawZPdDxAjrWkeFhon0OQ= -github.com/sigstore/sigstore v1.9.3/go.mod h1:VwYkiw0G0dRtwL25KSs04hCyVFF6CYMd/qvNeYrl7EQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sigstore/fulcio v1.7.1 h1:RcoW20Nz49IGeZyu3y9QYhyyV3ZKQ85T+FXPKkvE+aQ= +github.com/sigstore/fulcio v1.7.1/go.mod h1:7lYY+hsd8Dt+IvKQRC+KEhWpCZ/GlmNvwIa5JhypMS8= +github.com/sigstore/protobuf-specs v0.4.3 h1:kRgJ+ciznipH9xhrkAbAEHuuxD3GhYnGC873gZpjJT4= +github.com/sigstore/protobuf-specs v0.4.3/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU= +github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= -github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -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/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +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/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -504,90 +478,87 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= -github.com/vbauerster/mpb/v8 v8.9.3 h1:PnMeF+sMvYv9u23l6DO6Q3+Mdj408mjLRXIzmUmU2Z8= -github.com/vbauerster/mpb/v8 v8.9.3/go.mod h1:hxS8Hz4C6ijnppDSIX6LjG8FYJSoPo9iIOcE53Zik0c= +github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM= +github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= -go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= -go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= -go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= -go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= -go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= +go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= +go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= +go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= +go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= -go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= +go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= +go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= +go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/exporters/prometheus v0.58.0 h1:CJAxWKFIqdBennqxJyOgnt5LqkeFRT+Mz3Yjz3hL+h8= +go.opentelemetry.io/otel/exporters/prometheus v0.58.0/go.mod h1:7qo/4CLI+zYSNbv0GMNquzuss2FVZo3OYrGh96n4HNc= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= +go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= +go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= +go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.podman.io/common v0.65.0 h1:8JNl25U4VpKDkFHSymSPm4te7ZQHJbfAB/l2FqtmYEg= +go.podman.io/common v0.65.0/go.mod h1:+lJu8KHeoDQsD9HDdiFaMaOUiqPLQnK406WuLnqM7Z0= +go.podman.io/image/v5 v5.38.0 h1:aUKrCANkPvze1bnhLJsaubcfz0d9v/bSDLnwsXJm6G4= +go.podman.io/image/v5 v5.38.0/go.mod h1:hSIoIUzgBnmc4DjoIdzk63aloqVbD7QXDMkSE/cvG90= +go.podman.io/storage v1.61.0 h1:5hD/oyRYt1f1gxgvect+8syZBQhGhV28dCw2+CZpx0Q= +go.podman.io/storage v1.61.0/go.mod h1:A3UBK0XypjNZ6pghRhuxg62+2NIm5lcUGv/7XyMhMUI= 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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -596,7 +567,10 @@ 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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= @@ -604,12 +578,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -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/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= -golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -620,15 +594,13 @@ 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.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -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/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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-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-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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -640,14 +612,13 @@ 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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -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/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -656,15 +627,12 @@ 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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -679,9 +647,9 @@ 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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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= @@ -690,9 +658,9 @@ 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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -701,11 +669,11 @@ 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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= @@ -718,32 +686,38 @@ 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= -google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 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= @@ -753,19 +727,19 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 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.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/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.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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.1/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= @@ -773,55 +747,61 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= -helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= +helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= +helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= -k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= -k8s.io/component-helpers v0.32.3 h1:9veHpOGTPLluqU4hAu5IPOwkOIZiGAJUhHndfVc5FT4= -k8s.io/component-helpers v0.32.3/go.mod h1:utTBXk8lhkJewBKNuNf32Xl3KT/0VV19DmiXU/SV4Ao= -k8s.io/controller-manager v0.32.3 h1:jBxZnQ24k6IMeWLyxWZmpa3QVS7ww+osAIzaUY/jqyc= -k8s.io/controller-manager v0.32.3/go.mod h1:out1L3DZjE/p7JG0MoMMIaQGWIkt3c+pKaswqSHgKsI= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= +k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= +k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= +k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/component-helpers v0.34.0 h1:5T7P9XGMoUy1JDNKzHf0p/upYbeUf8ZaSf9jbx0QlIo= +k8s.io/component-helpers v0.34.0/go.mod h1:kaOyl5tdtnymriYcVZg4uwDBe2d1wlIpXyDkt6sVnt4= +k8s.io/controller-manager v0.34.0 h1:oCHoqS8dcFp7zDSu7HUvTpakq3isSxil3GprGGlJMsE= +k8s.io/controller-manager v0.34.0/go.mod h1:XFto21U+Mm9BT8r/Jd5E4tHCGtwjKAUFOuDcqaj2VK0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= -k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= -k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= -k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= -k8s.io/kubernetes v1.32.3 h1:2A58BlNME8NwsMawmnM6InYo3Jf35Nw5G79q46kXwoA= -k8s.io/kubernetes v1.32.3/go.mod h1:GvhiBeolvSRzBpFlgM0z/Bbu3Oxs9w3P6XfEgYaMi8k= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= -oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= +k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= +k8s.io/kubernetes v1.34.0 h1:NvUrwPAVB4W3mSOpJ/RtNGHWWYyUP/xPaX5rUSpzA0w= +k8s.io/kubernetes v1.34.0/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 h1:uOuSLOMBWkJH0TWa9X6l+mj5nZdm6Ay6Bli8HL8rNfk= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +pkg.package-operator.run/boxcutter v0.7.1 h1:us5wn0px9aAkumrXiQx38+Sc9dTgKJsHFbePoRQeWRo= +pkg.package-operator.run/boxcutter v0.7.1/go.mod h1:xEOKM3e3xtfSKYIOssQnu6DOWceiIu52qziMDcmUmpI= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-tools v0.19.0 h1:OU7jrPPiZusryu6YK0jYSjPqg8Vhf8cAzluP9XGI5uk= +sigs.k8s.io/controller-tools v0.19.0/go.mod h1:y5HY/iNDFkmFla2CfQoVb2AQXMsBk4ad84iR1PLANB0= +sigs.k8s.io/crdify v0.5.0 h1:mrMH9CgXQPTZUpTU6Klqfnlys8bggv/7uvLT2lXSP7A= +sigs.k8s.io/crdify v0.5.0/go.mod h1:ZIFxaYNgKYmFtZCLPysncXQ8oqwnNlHQbRUfxJHZwzU= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= -sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= -sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= -sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= -sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= -sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +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.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/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/OWNERS b/hack/OWNERS new file mode 100644 index 0000000000..835cabe507 --- /dev/null +++ b/hack/OWNERS @@ -0,0 +1,2 @@ +approvers: + - ci-approvers diff --git a/hack/ci/custom-linters/analyzers/testdata/go.mod b/hack/ci/custom-linters/analyzers/testdata/go.mod index 23e8719ca9..23875e2335 100644 --- a/hack/ci/custom-linters/analyzers/testdata/go.mod +++ b/hack/ci/custom-linters/analyzers/testdata/go.mod @@ -1,5 +1,5 @@ module testdata -go 1.23.4 +go 1.24.3 -require github.com/go-logr/logr v1.4.2 +require github.com/go-logr/logr v1.4.3 diff --git a/hack/ci/custom-linters/analyzers/testdata/go.sum b/hack/ci/custom-linters/analyzers/testdata/go.sum index dc2221787d..7ef38a47f2 100644 --- a/hack/ci/custom-linters/analyzers/testdata/go.sum +++ b/hack/ci/custom-linters/analyzers/testdata/go.sum @@ -1,2 +1,4 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/hack/demo/own-namespace-demo.sh b/hack/demo/own-namespace-demo-script.sh similarity index 65% rename from hack/demo/own-namespace-demo.sh rename to hack/demo/own-namespace-demo-script.sh index 6b867fbad0..86b3d28760 100755 --- a/hack/demo/own-namespace-demo.sh +++ b/hack/demo/own-namespace-demo-script.sh @@ -3,12 +3,17 @@ # # Welcome to the OwnNamespace install mode demo # -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +set -e +trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# enable 'SingleOwnNamespaceInstallSupport' feature gate -kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' +# install standard CRDs +echo "Install standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" -# wait for operator-controller to become available +# wait for standard CRDs to be available +kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io + +# Ensure controller is healthy kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -41,3 +46,16 @@ kubectl get deployments -n argocd-system argocd-operator-controller-manager -o j # check service account for role binding is the same as controller service-account rolebinding=$(kubectl get rolebindings -n argocd-system -o name | grep 'argocd-operator' | head -n 1) kubectl get -n argocd-system $rolebinding -o jsonpath='{.subjects}' | jq .[0] + +echo "Demo completed successfully!" + +# cleanup resources +echo "Cleaning up demo resources..." +kubectl delete clusterextension argocd-operator --ignore-not-found=true +kubectl delete namespace argocd-system --ignore-not-found=true +kubectl delete clusterrolebinding argocd-installer-crb --ignore-not-found=true + +# wait for operator-controller to become available with standard config +kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager + +echo "Demo cleanup completed!" diff --git a/hack/demo/resources/own-namespace-demo.yaml b/hack/demo/resources/own-namespace-demo.yaml index f1d6da927b..b22db7aa04 100644 --- a/hack/demo/resources/own-namespace-demo.yaml +++ b/hack/demo/resources/own-namespace-demo.yaml @@ -2,9 +2,6 @@ apiVersion: olm.operatorframework.io/v1 kind: ClusterExtension metadata: name: argocd-operator - annotations: - # watch namespace is the same as intall namespace - olm.operatorframework.io/watch-namespace: argocd-system spec: namespace: argocd-system serviceAccount: diff --git a/hack/demo/resources/single-namespace-demo.yaml b/hack/demo/resources/single-namespace-demo.yaml index 2403bc6a8b..9c1ac17f9f 100644 --- a/hack/demo/resources/single-namespace-demo.yaml +++ b/hack/demo/resources/single-namespace-demo.yaml @@ -2,13 +2,14 @@ apiVersion: olm.operatorframework.io/v1 kind: ClusterExtension metadata: name: argocd-operator - annotations: - # watch-namespace is different from install namespace - olm.operatorframework.io/watch-namespace: argocd spec: namespace: argocd-system serviceAccount: name: argocd-installer + config: + configType: Inline + inline: + watchNamespace: argocd source: sourceType: Catalog catalog: diff --git a/hack/demo/resources/webhook-provider-certmanager/mutating-webhook-test.yaml b/hack/demo/resources/webhook-provider-certmanager/mutating-webhook-test.yaml new file mode 100644 index 0000000000..571940204a --- /dev/null +++ b/hack/demo/resources/webhook-provider-certmanager/mutating-webhook-test.yaml @@ -0,0 +1,7 @@ +apiVersion: webhook.operators.coreos.io/v1 +kind: webhooktest +metadata: + namespace: webhook-operator + name: mutating-webhook-test +spec: + valid: true diff --git a/hack/demo/resources/webhook-provider-certmanager/validating-webhook-test.yaml b/hack/demo/resources/webhook-provider-certmanager/validating-webhook-test.yaml new file mode 100644 index 0000000000..227ab8417f --- /dev/null +++ b/hack/demo/resources/webhook-provider-certmanager/validating-webhook-test.yaml @@ -0,0 +1,7 @@ +apiVersion: webhook.operators.coreos.io/v1 +kind: webhooktest +metadata: + namespace: webhook-operator + name: validating-webhook-test +spec: + valid: false diff --git a/hack/demo/resources/webhook-provider-certmanager/webhook-operator-catalog.yaml b/hack/demo/resources/webhook-provider-certmanager/webhook-operator-catalog.yaml new file mode 100644 index 0000000000..ff325c0646 --- /dev/null +++ b/hack/demo/resources/webhook-provider-certmanager/webhook-operator-catalog.yaml @@ -0,0 +1,9 @@ +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: webhook-operator-catalog +spec: + source: + type: Image + image: + ref: quay.io/operator-framework/webhook-operator-index:0.0.3 diff --git a/hack/demo/resources/webhook-provider-certmanager/webhook-operator-extension.yaml b/hack/demo/resources/webhook-provider-certmanager/webhook-operator-extension.yaml new file mode 100644 index 0000000000..19b7eceb08 --- /dev/null +++ b/hack/demo/resources/webhook-provider-certmanager/webhook-operator-extension.yaml @@ -0,0 +1,15 @@ +apiVersion: olm.operatorframework.io/v1 +kind: ClusterExtension +metadata: + name: webhook-operator +spec: + namespace: webhook-operator + serviceAccount: + name: webhook-operator-installer + source: + catalog: + packageName: webhook-operator + version: 0.0.1 + selector: {} + upgradeConstraintPolicy: CatalogProvided + sourceType: Catalog diff --git a/hack/demo/single-own-namespace.sh b/hack/demo/single-namespace-demo-script.sh similarity index 65% rename from hack/demo/single-own-namespace.sh rename to hack/demo/single-namespace-demo-script.sh index 4e243f9df5..885854dd9d 100755 --- a/hack/demo/single-own-namespace.sh +++ b/hack/demo/single-namespace-demo-script.sh @@ -3,12 +3,17 @@ # # Welcome to the SingleNamespace install mode demo # -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +set -e +trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# enable 'SingleOwnNamespaceInstallSupport' feature gate -kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' +# install standard CRDs +echo "Install standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" -# wait for operator-controller to become available +# wait for standard CRDs to be available +kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io + +# Ensure controller is healthy kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -44,3 +49,16 @@ kubectl get deployments -n argocd-system argocd-operator-controller-manager -o j # check service account for role binding is the controller deployment service account rolebinding=$(kubectl get rolebindings -n argocd -o name | grep 'argocd-operator' | head -n 1) kubectl get -n argocd $rolebinding -o jsonpath='{.subjects}' | jq .[0] + +echo "Demo completed successfully!" + +# cleanup resources +echo "Cleaning up demo resources..." +kubectl delete clusterextension argocd-operator --ignore-not-found=true +kubectl delete namespace argocd-system argocd --ignore-not-found=true +kubectl delete clusterrolebinding argocd-installer-crb --ignore-not-found=true + +# wait for operator-controller to become available with standard config +kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager + +echo "Demo cleanup completed!" diff --git a/hack/demo/synthetic-user-cluster-admin-demo.sh b/hack/demo/synthetic-user-cluster-admin-demo-script.sh similarity index 100% rename from hack/demo/synthetic-user-cluster-admin-demo.sh rename to hack/demo/synthetic-user-cluster-admin-demo-script.sh diff --git a/hack/demo/webhook-provider-certmanager-demo-script.sh b/hack/demo/webhook-provider-certmanager-demo-script.sh new file mode 100755 index 0000000000..ba723ca6a8 --- /dev/null +++ b/hack/demo/webhook-provider-certmanager-demo-script.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# +# Welcome to the webhook support with CertManager demo +# +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT + +# enable 'WebhookProviderCertManager' feature +kubectl kustomize config/overlays/featuregate/webhook-provider-certmanager | kubectl apply -f - + +# wait for operator-controller to become available +kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager + +# create webhook-operator catalog +cat ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/webhook-operator-catalog.yaml +kubectl apply -f ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/webhook-operator-catalog.yaml + +# wait for catalog to be serving +kubectl wait --for=condition=Serving clustercatalog/webhook-operator-catalog --timeout="60s" + +# create install namespace +kubectl create ns webhook-operator + +# create installer service account +kubectl create serviceaccount -n webhook-operator webhook-operator-installer + +# give installer service account admin privileges +kubectl create clusterrolebinding webhook-operator-installer-crb --clusterrole=cluster-admin --serviceaccount=webhook-operator:webhook-operator-installer + +# install webhook operator clusterextension +cat ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/webhook-operator-extension.yaml + +# apply cluster extension +kubectl apply -f ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/webhook-operator-extension.yaml + +# wait for cluster extension installation to succeed +kubectl wait --for=condition=Installed clusterextension/webhook-operator --timeout="60s" + +# wait for webhook-operator deployment to become available and back the webhook service +kubectl wait --for=condition=Available -n webhook-operator deployments/webhook-operator-webhook + +# demonstrate working validating webhook +cat ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/validating-webhook-test.yaml + +# resource creation should be rejected by the validating webhook due to bad attribute value +kubectl apply -f ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/validating-webhook-test.yaml + +# demonstrate working mutating webhook +cat ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/mutating-webhook-test.yaml + +# apply resource +kubectl apply -f ${DEMO_RESOURCE_DIR}/webhook-provider-certmanager/mutating-webhook-test.yaml + +# get webhooktest resource in v1 schema - resource should have new .spec.mutate attribute +kubectl get webhooktest.v1.webhook.operators.coreos.io -n webhook-operator mutating-webhook-test -o yaml + +# demonstrate working conversion webhook by getting webhook test resource in v2 schema - the .spec attributes should now be under the .spec.conversion stanza +kubectl get webhooktest.v2.webhook.operators.coreos.io -n webhook-operator mutating-webhook-test -o yaml + +# this concludes the webhook support demo - Thank you! diff --git a/hack/kind-config/containerd/certs.d/docker-registry.operator-controller-e2e.svc.cluster.local:5000/hosts.toml b/hack/kind-config/containerd/certs.d/docker-registry.operator-controller-e2e.svc.cluster.local:5000/hosts.toml new file mode 100644 index 0000000000..b0a5eb47fc --- /dev/null +++ b/hack/kind-config/containerd/certs.d/docker-registry.operator-controller-e2e.svc.cluster.local:5000/hosts.toml @@ -0,0 +1,3 @@ +[host."https://localhost:30000/"] + capabilities = ["pull", "resolve"] + skip_verify = true diff --git a/hack/kind-config/containerd/certs.d/go.mod b/hack/kind-config/containerd/certs.d/go.mod new file mode 100644 index 0000000000..2cf82571b4 --- /dev/null +++ b/hack/kind-config/containerd/certs.d/go.mod @@ -0,0 +1,5 @@ +module hack-cert.d +// This file is present in the certs.d directory to ensure that +// certs.d/host:port directories are not included in the main go +// module. Go modules are not allowed to contain files with ':' +// in their name. diff --git a/hack/test/e2e-coverage.sh b/hack/test/e2e-coverage.sh index a5107ae12c..49c8db3d7c 100755 --- a/hack/test/e2e-coverage.sh +++ b/hack/test/e2e-coverage.sh @@ -2,19 +2,25 @@ set -euo pipefail -COVERAGE_OUTPUT="${COVERAGE_OUTPUT:-${ROOT_DIR}/coverage/e2e.out}" +COVERAGE_NAME="${COVERAGE_NAME:-e2e}" OPERATOR_CONTROLLER_NAMESPACE="olmv1-system" OPERATOR_CONTROLLER_MANAGER_DEPLOYMENT_NAME="operator-controller-controller-manager" + +CATALOGD_NAMESPACE="olmv1-system" +CATALOGD_MANAGER_DEPLOYMENT_NAME="catalogd-controller-manager" + COPY_POD_NAME="e2e-coverage-copy-pod" # Create a temporary directory for coverage -COVERAGE_DIR=${ROOT_DIR}/coverage/e2e +COVERAGE_OUTPUT=${ROOT_DIR}/coverage/${COVERAGE_NAME}.out +COVERAGE_DIR=${ROOT_DIR}/coverage/${COVERAGE_NAME} rm -rf ${COVERAGE_DIR} && mkdir -p ${COVERAGE_DIR} # Coverage-instrumented binary produces coverage on termination, # so we scale down the manager before gathering the coverage kubectl -n "$OPERATOR_CONTROLLER_NAMESPACE" scale deployment/"$OPERATOR_CONTROLLER_MANAGER_DEPLOYMENT_NAME" --replicas=0 +kubectl -n "$CATALOGD_NAMESPACE" scale deployment/"$CATALOGD_MANAGER_DEPLOYMENT_NAME" --replicas=0 # Wait for the copy pod to be ready kubectl -n "$OPERATOR_CONTROLLER_NAMESPACE" wait --for=condition=ready pod "$COPY_POD_NAME" diff --git a/hack/test/install-prometheus.sh b/hack/test/install-prometheus.sh new file mode 100755 index 0000000000..c9d7e0b1c4 --- /dev/null +++ b/hack/test/install-prometheus.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +source ".bingo/variables.env" + +set -euo pipefail + +help="install-prometheus.sh is used to set up prometheus monitoring for e2e testing. +Usage: + install-prometheus.sh [PROMETHEUS_NAMESPACE] [PROMETHEUS_VERSION] [GIT_VERSION] +" + +if [[ "$#" -ne 3 ]]; then + echo "Illegal number of arguments passed" + echo "${help}" + exit 1 +fi + +PROMETHEUS_NAMESPACE="$1" +PROMETHEUS_VERSION="$2" +GIT_VERSION="$3" + +TMPDIR="$(mktemp -d)" +trap 'echo "Cleaning up $TMPDIR"; rm -rf "$TMPDIR"' EXIT + +echo "Downloading Prometheus resources..." +curl -s "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/refs/tags/$%7BPROMETHEUS_VERSION%7D/kustomization.yaml" > "${TMPDIR}/kustomization.yaml" +curl -s "https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/refs/tags/$%7BPROMETHEUS_VERSION%7D/bundle.yaml" > "${TMPDIR}/bundle.yaml" + +echo "Patching namespace to ${PROMETHEUS_NAMESPACE}..." +(cd "$TMPDIR" && ${KUSTOMIZE} edit set namespace "$PROMETHEUS_NAMESPACE") + +echo "Applying Prometheus base..." +kubectl apply -k "$TMPDIR" --server-side + +echo "Waiting for Prometheus Operator pod to become ready..." +kubectl wait --for=condition=Ready pod -n "$PROMETHEUS_NAMESPACE" -l app.kubernetes.io/name=prometheus-operator + +echo "Applying prometheus Helm chart..." +${HELM} template prometheus helm/prometheus | sed "s/cert-git-version/cert-${VERSION}/g" | kubectl apply -f - + +echo "Waiting for metrics scraper to become ready..." +kubectl wait --for=create pods -n "$PROMETHEUS_NAMESPACE" prometheus-prometheus-0 --timeout=60s +kubectl wait --for=condition=Ready pods -n "$PROMETHEUS_NAMESPACE" prometheus-prometheus-0 --timeout=120s + +echo "Prometheus deployment completed successfully." diff --git a/hack/test/pre-upgrade-setup.sh b/hack/test/pre-upgrade-setup.sh index f8c3fee95e..669f9da37f 100755 --- a/hack/test/pre-upgrade-setup.sh +++ b/hack/test/pre-upgrade-setup.sh @@ -102,6 +102,18 @@ rules: - "watch" - "bind" - "escalate" + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - "olm.operatorframework.io" resources: diff --git a/hack/tools/crd-generator/README.md b/hack/tools/crd-generator/README.md new file mode 100644 index 0000000000..4334721672 --- /dev/null +++ b/hack/tools/crd-generator/README.md @@ -0,0 +1,53 @@ +# Guide to Operator-Controller CRD extensions + +All operator-controller (`opcon` for short) extensions to CRDs are part of the +comments to fields within the APIs. The fields look like XML tags, to distinguish +them from kubebuilder tags. + +All tags start with `` and have additional fields in between. +Usually the second field is `experimental`. Some tags may have an end tag (like XML) +that starts with `` + +The field that follows is experimental, and is not included in the standard CRD. It *is* included +in the experimental CRD. + +## Experimental Validation + +* Tag: `` +* Tag: `` + +A standard and/or experimental validation which may differ from one another. For example, where the +experimental CRD has extra enumerations. + +Where `VALIDATION` is one of: + +* `Enum=list;of;enums` + +A semi-colon separated list of enumerations, similar to the `+kubebuilder:validation:Enum` scheme. + +* `XValidation:message="something",rule="something"` + +An XValidation scheme, similar to the `+kubebuilder:validation:XValidation` scheme, but more limited. + +## Experimental Description + +* Start Tag: `` +* End Tag: `` + +Descriptive text that is only included as part of the field description within the experimental CRD. +All text between the tags is included in the experimental CRD, but removed from the standard CRD. + +This is only useful if the field is included in the standard CRD, but there's additional meaning in +the experimental CRD when feature gates are enabled. + +## Exclude from CRD Description + +* Start Tag: `` +* End Tag: `` + +Descriptive text that is excluded from the CRD description. This is similar to the use of `---`, except +the three hypens excludes *all* following text. diff --git a/hack/tools/crd-generator/main.go b/hack/tools/crd-generator/main.go new file mode 100644 index 0000000000..9687489f45 --- /dev/null +++ b/hack/tools/crd-generator/main.go @@ -0,0 +1,318 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "regexp" + "strings" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-tools/pkg/crd" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" + "sigs.k8s.io/yaml" +) + +const ( + // FeatureSetAnnotation is the annotation key used in the Operator-Controller API CRDs to specify + // the installed Operator-Controller API channel. + GeneratorAnnotation = "olm.operatorframework.io/generator" + VersionAnnotation = "controller-gen.kubebuilder.io/version" + StandardChannel = "standard" + ExperimentalChannel = "experimental" +) + +var standardKinds = map[string]bool{ + "ClusterExtension": true, + "ClusterCatalog": true, +} + +// This generation code is largely copied from below into operator-controller +// github.com/kubernetes-sigs/gateway-api/blob/b7d2c5788bf38fc2c18085de524e204034c69a14/pkg/generator/main.go +// This generation code is largely copied from below into gateway-api +// github.com/kubernetes-sigs/controller-tools/blob/ab52f76cc7d167925b2d5942f24bf22e30f49a02/pkg/crd/gen.go +func main() { + runGenerator(os.Args[1:]...) +} + +func runGenerator(args ...string) { + outputDir := "config/crd" + ctVer := "" + crdRoot := "github.com/operator-framework/operator-controller/api/v1" + if len(args) >= 1 { + // Get the output directory + outputDir = args[0] + } + if len(args) >= 2 { + // get the controller-tools version + ctVer = args[1] + } + if len(args) >= 3 { + crdRoot = args[2] + } + + roots, err := loader.LoadRoots( + "k8s.io/apimachinery/pkg/runtime/schema", // Needed to parse generated register functions. + crdRoot, + ) + if err != nil { + log.Fatalf("failed to load package roots: %s", err) + } + + generator := &crd.Generator{} + + parser := &crd.Parser{ + Collector: &markers.Collector{Registry: &markers.Registry{}}, + Checker: &loader.TypeChecker{ + NodeFilters: []loader.NodeFilter{generator.CheckFilter()}, + }, + } + + err = generator.RegisterMarkers(parser.Collector.Registry) + if err != nil { + log.Fatalf("failed to register markers: %s", err) + } + + crd.AddKnownTypes(parser) + for _, r := range roots { + parser.NeedPackage(r) + } + + metav1Pkg := crd.FindMetav1(roots) + if metav1Pkg == nil { + log.Fatalf("no objects in the roots, since nothing imported metav1") + } + + kubeKinds := crd.FindKubeKinds(parser, metav1Pkg) + if len(kubeKinds) == 0 { + log.Fatalf("no objects in the roots") + } + + channels := []string{StandardChannel, ExperimentalChannel} + for _, channel := range channels { + for _, groupKind := range kubeKinds { + if channel == StandardChannel && !standardKinds[groupKind.Kind] { + continue + } + + log.Printf("generating %s CRD for %v\n", channel, groupKind) + + parser.NeedCRDFor(groupKind, nil) + crdRaw := parser.CustomResourceDefinitions[groupKind] + + // Inline version of "addAttribution(&crdRaw)" ... + if crdRaw.Annotations == nil { + crdRaw.Annotations = map[string]string{} + } + crdRaw.Annotations[GeneratorAnnotation] = channel + if ctVer != "" { + crdRaw.Annotations[VersionAnnotation] = ctVer + } + + // Prevent the top level metadata for the CRD to be generated regardless of the intention in the arguments + crd.FixTopLevelMetadata(crdRaw) + + channelCrd := crdRaw.DeepCopy() + for i, version := range channelCrd.Spec.Versions { + if channel == StandardChannel && strings.Contains(version.Name, "alpha") { + channelCrd.Spec.Versions[i].Served = false + } + version.Schema.OpenAPIV3Schema.Properties = opconTweaksMap(channel, version.Schema.OpenAPIV3Schema.Properties) + } + + conv, err := crd.AsVersion(*channelCrd, apiextensionsv1.SchemeGroupVersion) + if err != nil { + log.Fatalf("failed to convert CRD: %s", err) + } + + out, err := yaml.Marshal(conv) + if err != nil { + log.Fatalf("failed to marshal CRD: %s", err) + } + + // Do some filtering of the resulting YAML + var yamlData map[string]any + err = yaml.Unmarshal(out, &yamlData) + if err != nil { + log.Fatalf("failed to unmarshal data: %s", err) + } + + scrapYaml(yamlData, "status") + scrapYaml(yamlData, "metadata", "creationTimestamp") + + out, err = yaml.Marshal(yamlData) + if err != nil { + log.Fatalf("failed to re-marshal CRD: %s", err) + } + + // If missing, add a break at the beginning of the file + breakLine := []byte("---\n") + if !bytes.HasPrefix(out, breakLine) { + out = append(breakLine, out...) + } + + fileName := fmt.Sprintf("%s/%s/%s_%s.yaml", outputDir, channel, crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) + err = os.WriteFile(fileName, out, 0o600) + if err != nil { + log.Fatalf("failed to write CRD: %s", err) + } + } + } +} + +func opconTweaksMap(channel string, props map[string]apiextensionsv1.JSONSchemaProps) map[string]apiextensionsv1.JSONSchemaProps { + for name := range props { + jsonProps := props[name] + p := opconTweaks(channel, name, jsonProps) + if p == nil { + delete(props, name) + } else { + props[name] = *p + } + } + return props +} + +// Custom Opcon API Tweaks for tags prefixed with `") { + return nil + } + } + + // TODO(robscott): Figure out why crdgen switched this to "object" + if jsonProps.Format == "date-time" { + jsonProps.Type = "string" + } + + validationPrefix := fmt.Sprintf(" 0 { + enumRe := regexp.MustCompile(validationPrefix + "Enum=([A-Za-z;]*)>") + enumMatches := enumRe.FindAllStringSubmatch(jsonProps.Description, 64) + for _, enumMatch := range enumMatches { + if len(enumMatch) != 2 { + log.Fatalf("Invalid %s Enum tag for %s", validationPrefix, name) + } + + numValid++ + jsonProps.Enum = []apiextensionsv1.JSON{} + for _, val := range strings.Split(enumMatch[1], ";") { + jsonProps.Enum = append(jsonProps.Enum, apiextensionsv1.JSON{Raw: []byte("\"" + val + "\"")}) + } + } + + celRe := regexp.MustCompile(validationPrefix + "XValidation:rule=\"([^\"]*)\",message=\"([^\"]*)\">") + celMatches := celRe.FindAllStringSubmatch(jsonProps.Description, 64) + for _, celMatch := range celMatches { + if len(celMatch) != 3 { + log.Fatalf("Invalid %s CEL tag for %s", validationPrefix, name) + } + + numValid++ + jsonProps.XValidations = append(jsonProps.XValidations, apiextensionsv1.ValidationRule{ + Message: celMatch[1], + Rule: celMatch[2], + }) + } + } + + if numValid < numExpressions { + log.Fatalf("Found %d Opcon validation expressions, but only %d were valid", numExpressions, numValid) + } + + jsonProps.Description = formatDescription(jsonProps.Description, channel, name) + + if len(jsonProps.Properties) > 0 { + jsonProps.Properties = opconTweaksMap(channel, jsonProps.Properties) + } else if jsonProps.Items != nil && jsonProps.Items.Schema != nil { + jsonProps.Items.Schema = opconTweaks(channel, name, *jsonProps.Items.Schema) + } + + return &jsonProps +} + +func formatDescription(description string, channel string, name string) string { + startTag := "" + endTag := "" + if channel == StandardChannel && strings.Contains(description, startTag) { + regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*` + re := regexp.MustCompile(regexPattern) + match := re.FindStringSubmatch(description) + if len(match) != 2 { + log.Fatalf("Invalid tag for %s", name) + } + description = re.ReplaceAllString(description, "\n\n") + } else { + description = strings.ReplaceAll(description, startTag, "") + description = strings.ReplaceAll(description, endTag, "") + } + + // Comments within "opcon:util:excludeFromCRD" tag are not included in the generated CRD and all trailing \n operators before + // and after the tags are removed and replaced with three \n operators. + startTag = "" + endTag = "" + if strings.Contains(description, startTag) { + regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*` + re := regexp.MustCompile(regexPattern) + match := re.FindStringSubmatch(description) + if len(match) != 2 { + log.Fatalf("Invalid tag for %s", name) + } + description = re.ReplaceAllString(description, "\n\n\n") + } + + opconRe := regexp.MustCompile(``) + description = opconRe.ReplaceAllLiteralString(description, "") + + // Remove anything following three hyphens + regexPattern := `(?s)---.*` + re := regexp.MustCompile(regexPattern) + description = re.ReplaceAllString(description, "") + + // Remove any extra \n (more than 2 and all trailing at the end) + regexPattern = `\n\n+` + re = regexp.MustCompile(regexPattern) + description = re.ReplaceAllString(description, "\n\n") + description = strings.Trim(description, "\n") + + return description +} + +// delete a field in unstructured YAML +func scrapYaml(data map[string]any, fields ...string) { + if len(fields) == 0 { + return + } + if len(fields) == 1 { + delete(data, fields[0]) + return + } + if f, ok := data[fields[0]]; ok { + if f2, ok := f.(map[string]any); ok { + scrapYaml(f2, fields[1:]...) + } + } +} diff --git a/hack/tools/crd-generator/main_test.go b/hack/tools/crd-generator/main_test.go new file mode 100644 index 0000000000..d2eb28d61f --- /dev/null +++ b/hack/tools/crd-generator/main_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +const controllerToolsVersion = "v0.19.0" + +func TestRunGenerator(t *testing.T) { + here, err := os.Getwd() + require.NoError(t, err) + // Get to repo root + err = os.Chdir("../../..") + require.NoError(t, err) + defer func() { + _ = os.Chdir(here) + }() + dir, err := os.MkdirTemp("", "crd-generate-*") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.NoError(t, os.Mkdir(filepath.Join(dir, "standard"), 0o700)) + require.NoError(t, os.Mkdir(filepath.Join(dir, "experimental"), 0o700)) + runGenerator(dir, controllerToolsVersion) + + f1 := filepath.Join(dir, "standard/olm.operatorframework.io_clusterextensions.yaml") + f2 := "helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) + + f1 = filepath.Join(dir, "standard/olm.operatorframework.io_clustercatalogs.yaml") + f2 = "helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) + + f1 = filepath.Join(dir, "experimental/olm.operatorframework.io_clusterextensions.yaml") + f2 = "helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) + + f1 = filepath.Join(dir, "experimental/olm.operatorframework.io_clustercatalogs.yaml") + f2 = "helm/olmv1/base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) +} + +func TestTags(t *testing.T) { + here, err := os.Getwd() + require.NoError(t, err) + err = os.Chdir("testdata") + defer func() { + _ = os.Chdir(here) + }() + require.NoError(t, err) + dir, err := os.MkdirTemp("", "crd-generate-*") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.NoError(t, os.Mkdir(filepath.Join(dir, "standard"), 0o700)) + require.NoError(t, os.Mkdir(filepath.Join(dir, "experimental"), 0o700)) + runGenerator(dir, controllerToolsVersion, "github.com/operator-framework/operator-controller/hack/tools/crd-generator/testdata/api/v1") + + f1 := filepath.Join(dir, "standard/olm.operatorframework.io_clusterextensions.yaml") + f2 := "output/standard/olm.operatorframework.io_clusterextensions.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) + + f1 = filepath.Join(dir, "experimental/olm.operatorframework.io_clusterextensions.yaml") + f2 = "output/experimental/olm.operatorframework.io_clusterextensions.yaml" + fmt.Printf("comparing: %s to %s\n", f1, f2) + compareFiles(t, f1, f2) +} + +func compareFiles(t *testing.T, file1, file2 string) { + f1, err := os.Open(file1) + require.NoError(t, err) + defer func() { + _ = f1.Close() + }() + + f2, err := os.Open(file2) + require.NoError(t, err) + defer func() { + _ = f2.Close() + }() + + for { + b1 := make([]byte, 64000) + b2 := make([]byte, 64000) + n1, err1 := f1.Read(b1) + n2, err2 := f2.Read(b2) + + // Success if both have EOF at the same time + if err1 == io.EOF && err2 == io.EOF { + return + } + require.NoError(t, err1) + require.NoError(t, err2) + require.Equal(t, n1, n2) + require.Equal(t, b1, b2) + } +} diff --git a/hack/tools/crd-generator/testdata/api/v1/clusterextension_types.go b/hack/tools/crd-generator/testdata/api/v1/clusterextension_types.go new file mode 100644 index 0000000000..8e134a3429 --- /dev/null +++ b/hack/tools/crd-generator/testdata/api/v1/clusterextension_types.go @@ -0,0 +1,522 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ClusterExtensionKind = "ClusterExtension" + +type ( + UpgradeConstraintPolicy string + CRDUpgradeSafetyEnforcement string +) + +const ( + // The extension will only upgrade if the new version satisfies + // the upgrade constraints set by the package author. + UpgradeConstraintPolicyCatalogProvided UpgradeConstraintPolicy = "CatalogProvided" + + // Unsafe option which allows an extension to be + // upgraded or downgraded to any available version of the package and + // ignore the upgrade path designed by package authors. + // This assumes that users independently verify the outcome of the changes. + // Use with caution as this can lead to unknown and potentially + // disastrous results such as data loss. + UpgradeConstraintPolicySelfCertified UpgradeConstraintPolicy = "SelfCertified" +) + +// ClusterExtensionSpec defines the desired state of ClusterExtension +type ClusterExtensionSpec struct { + // namespace is a reference to a Kubernetes namespace. + // This is the namespace in which the provided ServiceAccount must exist. + // It also designates the default namespace where namespace-scoped resources + // for the extension are applied to the cluster. + // Some extensions may contain namespace-scoped resources to be applied in other namespaces. + // This namespace must exist. + // + // namespace is required, immutable, and follows the DNS label standard + // as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + // start and end with an alphanumeric character, and be no longer than 63 characters + // + // [RFC 1123]: https://tools.ietf.org/html/rfc1123 + // + // +kubebuilder:validation:MaxLength:=63 + // + // + // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\")",message="namespace must be a valid DNS1123 label" + // +kubebuilder:validation:Required + Namespace string `json:"namespace"` + + // serviceAccount is a reference to a ServiceAccount used to perform all interactions + // with the cluster that are required to manage the extension. + // The ServiceAccount must be configured with the necessary permissions to perform these interactions. + // The ServiceAccount must exist in the namespace referenced in the spec. + // serviceAccount is required. + // + // +kubebuilder:validation:Required + ServiceAccount ServiceAccountReference `json:"serviceAccount"` + + // source is a required field which selects the installation source of content + // for this ClusterExtension. Selection is performed by setting the sourceType. + // + // Catalog is currently the only implemented sourceType, and setting the + // sourcetype to "Catalog" requires the catalog field to also be defined. + // + // Below is a minimal example of a source definition (in yaml): + // + // source: + // sourceType: Catalog + // catalog: + // packageName: example-package + // + // +kubebuilder:validation:Required + Source SourceConfig `json:"source"` + + // install is an optional field used to configure the installation options + // for the ClusterExtension such as the pre-flight check configuration. + // + // +optional + Install *ClusterExtensionInstallConfig `json:"install,omitempty"` +} + +const SourceTypeCatalog = "Catalog" + +// SourceConfig is a discriminated union which selects the installation source. +// +// +union +// +kubebuilder:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Catalog' ? has(self.catalog) : !has(self.catalog)",message="catalog is required when sourceType is Catalog, and forbidden otherwise" +type SourceConfig struct { + // sourceType is a required reference to the type of install source. + // + // Allowed values are "Catalog" + // + // When this field is set to "Catalog", information for determining the + // appropriate bundle of content to install will be fetched from + // ClusterCatalog resources existing on the cluster. + // When using the Catalog sourceType, the catalog field must also be set. + // + // +unionDiscriminator + // + // + // +kubebuilder:validation:Required + SourceType string `json:"sourceType"` + + // catalog is used to configure how information is sourced from a catalog. + // This field is required when sourceType is "Catalog", and forbidden otherwise. + // + // + // This is the experimental description for Catalog + // + // + // + // No one should see this! + // + // + // +optional + Catalog *CatalogFilter `json:"catalog,omitempty"` + + // test is a required parameter + // + Test string `json:"test"` +} + +// ClusterExtensionInstallConfig is a union which selects the clusterExtension installation config. +// ClusterExtensionInstallConfig requires the namespace and serviceAccount which should be used for the installation of packages. +// +// +kubebuilder:validation:XValidation:rule="has(self.preflight)",message="at least one of [preflight] are required when install is specified" +// +union +type ClusterExtensionInstallConfig struct { + // preflight is an optional field that can be used to configure the checks that are + // run before installation or upgrade of the content for the package specified in the packageName field. + // + // When specified, it replaces the default preflight configuration for install/upgrade actions. + // When not specified, the default configuration will be used. + // + // +optional + Preflight *PreflightConfig `json:"preflight,omitempty"` +} + +// CatalogFilter defines the attributes used to identify and filter content from a catalog. +type CatalogFilter struct { + // packageName is a reference to the name of the package to be installed + // and is used to filter the content from catalogs. + // + // packageName is required, immutable, and follows the DNS subdomain standard + // as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + // hyphens (-) or periods (.), start and end with an alphanumeric character, + // and be no longer than 253 characters. + // + // Some examples of valid values are: + // - some-package + // - 123-package + // - 1-package-2 + // - somepackage + // + // Some examples of invalid values are: + // - -some-package + // - some-package- + // - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + // - some.package + // + // [RFC 1123]: https://tools.ietf.org/html/rfc1123 + // + // +kubebuilder:validation.Required + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="packageName is immutable" + // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" + // +kubebuilder:validation:Required + PackageName string `json:"packageName"` + + // version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + // + // Acceptable version ranges are no longer than 64 characters. + // Version ranges are composed of comma- or space-delimited values and one or + // more comparison operators, known as comparison strings. Additional + // comparison strings can be added using the OR operator (||). + // + // # Range Comparisons + // + // To specify a version range, you can use a comparison string like ">=3.0, + // <3.6". When specifying a range, automatic updates will occur within that + // range. The example comparison string means "install any version greater than + // or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + // upgrades are available within the version range after initial installation, + // those upgrades should be automatically performed. + // + // # Pinned Versions + // + // To specify an exact version to install you can use a version range that + // "pins" to a specific version. When pinning to a specific version, no + // automatic updates will occur. An example of a pinned version range is + // "0.6.0", which means "only install version 0.6.0 and never + // upgrade from this version". + // + // # Basic Comparison Operators + // + // The basic comparison operators and their meanings are: + // - "=", equal (not aliased to an operator) + // - "!=", not equal + // - "<", less than + // - ">", greater than + // - ">=", greater than OR equal to + // - "<=", less than OR equal to + // + // # Wildcard Comparisons + // + // You can use the "x", "X", and "*" characters as wildcard characters in all + // comparison operations. Some examples of using the wildcard characters: + // - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + // - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + // - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + // - "x", "X", and "*" is equivalent to ">= 0.0.0" + // + // # Patch Release Comparisons + // + // When you want to specify a minor version up to the next major version you + // can use the "~" character to perform patch comparisons. Some examples: + // - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + // - "~1" and "~1.x" is equivalent to ">=1, <2" + // - "~2.3" is equivalent to ">=2.3, <2.4" + // - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + // + // # Major Release Comparisons + // + // You can use the "^" character to make major release comparisons after a + // stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + // - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + // - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + // - "^2.3" is equivalent to ">=2.3, <3" + // - "^2.x" is equivalent to ">=2.0.0, <3" + // - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + // - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + // - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + // - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + // - "^0" is equivalent to ">=0.0.0, <1.0.0" + // + // # OR Comparisons + // You can use the "||" character to represent an OR operation in the version + // range. Some examples: + // - ">=1.2.3, <2.0.0 || >3.0.0" + // - "^0 || ^3 || ^5" + // + // For more information on semver, please see https://semver.org/ + // + // +kubebuilder:validation:MaxLength:=64 + // +kubebuilder:validation:XValidation:rule="self.matches(\"^(\\\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|[x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)((?:\\\\s+|,\\\\s*|\\\\s*\\\\|\\\\|\\\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\\\^)\\\\s*(v?(0|[1-9]\\\\d*|x|X|\\\\*])(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*))?(\\\\.(0|[1-9]\\\\d*|x|X|\\\\*]))?(-([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?(\\\\+([0-9A-Za-z\\\\-]+(\\\\.[0-9A-Za-z\\\\-]+)*))?)\\\\s*)*$\")",message="invalid version expression" + // +optional + Version string `json:"version,omitempty"` + + // channels is an optional reference to a set of channels belonging to + // the package specified in the packageName field. + // + // A "channel" is a package-author-defined stream of updates for an extension. + // + // Each channel in the list must follow the DNS subdomain standard + // as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + // hyphens (-) or periods (.), start and end with an alphanumeric character, + // and be no longer than 253 characters. No more than 256 channels can be specified. + // + // When specified, it is used to constrain the set of installable bundles and + // the automated upgrade path. This constraint is an AND operation with the + // version field. For example: + // - Given channel is set to "foo" + // - Given version is set to ">=1.0.0, <1.5.0" + // - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + // - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + // + // When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + // + // Some examples of valid values are: + // - 1.1.x + // - alpha + // - stable + // - stable-v1 + // - v1-stable + // - dev-preview + // - preview + // - community + // + // Some examples of invalid values are: + // - -some-channel + // - some-channel- + // - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + // - original_40 + // - --default-channel + // + // [RFC 1123]: https://tools.ietf.org/html/rfc1123 + // + // +kubebuilder:validation:items:MaxLength:=253 + // +kubebuilder:validation:MaxItems:=256 + // +kubebuilder:validation:items:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="channels entries must be valid DNS1123 subdomains" + // +optional + Channels []string `json:"channels,omitempty"` + + // selector is an optional field that can be used + // to filter the set of ClusterCatalogs used in the bundle + // selection process. + // + // When unspecified, all ClusterCatalogs will be used in + // the bundle selection process. + // + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty"` + + // upgradeConstraintPolicy is an optional field that controls whether + // the upgrade path(s) defined in the catalog are enforced for the package + // referenced in the packageName field. + // + // Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + // + // When this field is set to "CatalogProvided", automatic upgrades will only occur + // when upgrade constraints specified by the package author are met. + // + // When this field is set to "SelfCertified", the upgrade constraints specified by + // the package author are ignored. This allows for upgrades and downgrades to + // any version of the package. This is considered a dangerous operation as it + // can lead to unknown and potentially disastrous outcomes, such as data + // loss. It is assumed that users have independently verified changes when + // using this option. + // + // When this field is omitted, the default value is "CatalogProvided". + // + // +kubebuilder:validation:Enum:=CatalogProvided;SelfCertified + // +kubebuilder:default:=CatalogProvided + // +optional + UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"` +} + +// ServiceAccountReference identifies the serviceAccount used fo install a ClusterExtension. +type ServiceAccountReference struct { + // name is a required, immutable reference to the name of the ServiceAccount + // to be used for installation and management of the content for the package + // specified in the packageName field. + // + // This ServiceAccount must exist in the installNamespace. + // + // name follows the DNS subdomain standard as defined in [RFC 1123]. + // It must contain only lowercase alphanumeric characters, + // hyphens (-) or periods (.), start and end with an alphanumeric character, + // and be no longer than 253 characters. + // + // Some examples of valid values are: + // - some-serviceaccount + // - 123-serviceaccount + // - 1-serviceaccount-2 + // - someserviceaccount + // - some.serviceaccount + // + // Some examples of invalid values are: + // - -some-serviceaccount + // - some-serviceaccount- + // + // [RFC 1123]: https://tools.ietf.org/html/rfc1123 + // + // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="name must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" + // +kubebuilder:validation:Required + Name string `json:"name"` +} + +// PreflightConfig holds the configuration for the preflight checks. If used, at least one preflight check must be non-nil. +// +// +kubebuilder:validation:XValidation:rule="has(self.crdUpgradeSafety)",message="at least one of [crdUpgradeSafety] are required when preflight is specified" +type PreflightConfig struct { + // crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + // checks that run prior to upgrades of installed content. + // + // The CRD Upgrade Safety pre-flight check safeguards from unintended + // consequences of upgrading a CRD, such as data loss. + CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"` +} + +// CRDUpgradeSafetyPreflightConfig is the configuration for CRD upgrade safety preflight check. +type CRDUpgradeSafetyPreflightConfig struct { + // enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + // + // Allowed values are "None" or "Strict". The default value is "Strict". + // + // When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + // when performing an upgrade operation. This should be used with caution as + // unintended consequences such as data loss can occur. + // + // When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + // performing an upgrade operation. + // + // +kubebuilder:validation:Enum:="None";"Strict" + // +kubebuilder:validation:Required + Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"` +} + +const ( + // TypeDeprecated is a rollup condition that is present when + // any of the deprecated conditions are present. + TypeDeprecated = "Deprecated" + TypePackageDeprecated = "PackageDeprecated" + TypeChannelDeprecated = "ChannelDeprecated" + TypeBundleDeprecated = "BundleDeprecated" + + // None will not perform CRD upgrade safety checks. + CRDUpgradeSafetyEnforcementNone CRDUpgradeSafetyEnforcement = "None" + // Strict will enforce the CRD upgrade safety check and block the upgrade if the CRD would not pass the check. + CRDUpgradeSafetyEnforcementStrict CRDUpgradeSafetyEnforcement = "Strict" +) + +// BundleMetadata is a representation of the identifying attributes of a bundle. +type BundleMetadata struct { + // name is required and follows the DNS subdomain standard + // as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + // hyphens (-) or periods (.), start and end with an alphanumeric character, + // and be no longer than 253 characters. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="packageName must be a valid DNS1123 subdomain. It must contain only lowercase alphanumeric characters, hyphens (-) or periods (.), start and end with an alphanumeric character, and be no longer than 253 characters" + Name string `json:"name"` + + // version is a required field and is a reference to the version that this bundle represents + // version follows the semantic versioning standard as defined in https://semver.org/. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self.matches(\"^([0-9]+)(\\\\.[0-9]+)?(\\\\.[0-9]+)?(-([-0-9A-Za-z]+(\\\\.[-0-9A-Za-z]+)*))?(\\\\+([-0-9A-Za-z]+(-\\\\.[-0-9A-Za-z]+)*))?\")",message="version must be well-formed semver" + Version string `json:"version"` +} + +// ClusterExtensionStatus defines the observed state of a ClusterExtension. +type ClusterExtensionStatus struct { + // The set of condition types which apply to all spec.source variations are Installed and Progressing. + // + // The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + // When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + // When Installed is False and the Reason is Failed, the bundle has failed to install. + // + // The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + // When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + // When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + // When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + // + // When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + // These are indications from a package owner to guide users away from a particular package, channel, or bundle. + // BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + // ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + // PackageDeprecated is set if the requested package is marked deprecated in the catalog. + // Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + // + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // install is a representation of the current installation status for this ClusterExtension. + // + // +optional + Install *ClusterExtensionInstallStatus `json:"install,omitempty"` +} + +// ClusterExtensionInstallStatus is a representation of the status of the identified bundle. +type ClusterExtensionInstallStatus struct { + // bundle is a required field which represents the identifying attributes of a bundle. + // + // A "bundle" is a versioned set of content that represents the resources that + // need to be applied to a cluster to install a package. + // + // +kubebuilder:validation:Required + Bundle BundleMetadata `json:"bundle"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Installed Bundle",type=string,JSONPath=`.status.install.bundle.name` +// +kubebuilder:printcolumn:name=Version,type=string,JSONPath=`.status.install.bundle.version` +// +kubebuilder:printcolumn:name="Installed",type=string,JSONPath=`.status.conditions[?(@.type=='Installed')].status` +// +kubebuilder:printcolumn:name="Progressing",type=string,JSONPath=`.status.conditions[?(@.type=='Progressing')].status` +// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` + +// ClusterExtension is the Schema for the clusterextensions API +type ClusterExtension struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is an optional field that defines the desired state of the ClusterExtension. + // +optional + Spec ClusterExtensionSpec `json:"spec,omitempty"` + + // status is an optional field that defines the observed state of the ClusterExtension. + // +optional + Status ClusterExtensionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterExtensionList contains a list of ClusterExtension +type ClusterExtensionList struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items is a required list of ClusterExtension objects. + // + // +kubebuilder:validation:Required + Items []ClusterExtension `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterExtension{}, &ClusterExtensionList{}) +} diff --git a/hack/tools/crd-generator/testdata/api/v1/groupversion_info.go b/hack/tools/crd-generator/testdata/api/v1/groupversion_info.go new file mode 100644 index 0000000000..f2e8582ee5 --- /dev/null +++ b/hack/tools/crd-generator/testdata/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the olm v1 API group +// +kubebuilder:object:generate=true +// +groupName=olm.operatorframework.io +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "olm.operatorframework.io", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/hack/tools/crd-generator/testdata/output/experimental/olm.operatorframework.io_clusterextensions.yaml b/hack/tools/crd-generator/testdata/output/experimental/olm.operatorframework.io_clusterextensions.yaml new file mode 100644 index 0000000000..9866f68bbe --- /dev/null +++ b/hack/tools/crd-generator/testdata/output/experimental/olm.operatorframework.io_clusterextensions.yaml @@ -0,0 +1,597 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + - message: self == oldSelf + rule: namespace really is immutable + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + + This is the experimental description for Catalog + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + - NotCatalog + type: string + test: + description: test is a required parameter + type: string + required: + - sourceType + - test + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml similarity index 99% rename from config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml rename to hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml index a582917aae..8dcb4beed1 100644 --- a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml @@ -3,7 +3,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.3 + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard name: clusterextensions.olm.operatorframework.io spec: group: olm.operatorframework.io @@ -125,10 +126,10 @@ spec: maxLength: 63 type: string x-kubernetes-validations: - - message: namespace is immutable - rule: self == oldSelf - message: namespace must be a valid DNS1123 label rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + - message: self == oldSelf + rule: namespace is immutable serviceAccount: description: |- serviceAccount is a reference to a ServiceAccount used to perform all interactions @@ -450,6 +451,7 @@ spec: type: string required: - sourceType + - test type: object x-kubernetes-validations: - message: catalog is required when sourceType is Catalog, and forbidden diff --git a/hack/tools/update-crds.sh b/hack/tools/update-crds.sh new file mode 100755 index 0000000000..e379b59896 --- /dev/null +++ b/hack/tools/update-crds.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +set -e + +# This uses a custom CRD generator to create "standard" and "experimental" CRDs + +# The names of the generated CRDs +CE="olm.operatorframework.io_clusterextensions.yaml" +CC="olm.operatorframework.io_clustercatalogs.yaml" +CR="olm.operatorframework.io_clusterextensionrevisions.yaml" + +# order for modules and crds must match +# each item in crds must be unique, and should be associated with a module +modules=("operator-controller" "catalogd" "operator-controller") +crds=("${CE}" "${CC}" "${CR}") + +# Channels must much those in the generator +channels=("standard" "experimental") + +# Create the temp output directories +CRD_TMP=$(mktemp -d) +# Clean up the temp output directories +trap "rm -rf ${CRD_TMP}" EXIT + +for c in ${channels[@]}; do + mkdir -p ${CRD_TMP}/${c} +done + +# This calculates the controller-tools version, to keep the annotation correct +CONTROLLER_TOOLS_VER=$(go list -m sigs.k8s.io/controller-tools | awk '{print $2}') + +# Generate the CRDs +go run ./hack/tools/crd-generator ${CRD_TMP} ${CONTROLLER_TOOLS_VER} + +# Copy the generated files +for b in ${!modules[@]}; do + for c in ${channels[@]}; do + # CRDs for kinds not listed in the standardKinds map in crd-generator/main.go + # will not be generated for the standard channel - so we check the expected generated + # file exists before copying it. + FILE="${CRD_TMP}/${c}/${crds[${b}]}" + DST="helm/olmv1/base/${modules[${b}]}/crd/${c}" + if [ -e "${FILE}" ]; then + echo "$(date '+%Y/%m/%d %T') ${FILE} --> ${DST}" + mkdir -p "${DST}" + cp "${FILE}" "${DST}" + fi + done +done diff --git a/hack/tools/update-tls-profiles.sh b/hack/tools/update-tls-profiles.sh new file mode 100755 index 0000000000..8fa61c43ee --- /dev/null +++ b/hack/tools/update-tls-profiles.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -e + +if [ -z "${JQ}" ]; then + echo "JQ not defined" + exit 1 +fi + +OUTPUT=internal/shared/util/tlsprofiles/mozilla_data.go +INPUT=https://ssl-config.mozilla.org/guidelines/latest.json + +TMPFILE="$(mktemp)" +trap 'rm -rf "$TMPFILE"' EXIT + +curl -L -s ${INPUT} > ${TMPFILE} + +version=$(${JQ} -r '.version' ${TMPFILE}) + +cat > ${OUTPUT} <> ${OUTPUT} <> ${OUTPUT} + ${JQ} -r ".configurations.$1.ciphers.go[] | . |= \"tls.\" + . + \",\"" ${TMPFILE} >> ${OUTPUT} + + cat >> ${OUTPUT} <> ${OUTPUT} + + version=$(${JQ} -r ".configurations.$1.tls_versions[0]" ${TMPFILE}) + version=${version/TLSv1./tls.VersionTLS1} + version=${version/TLSv1/tls.VersionTLS10} + + cat >> ${OUTPUT} <= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + x-kubernetes-validations: + - message: cannot specify pollIntervalMinutes while using digest-based + image + rule: 'self.ref.find(''(@.*:)'') != "" ? !has(self.pollIntervalMinutes) + : true' + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When using an image source, the image field must be set and must be the only field defined for this type. + enum: + - Image + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + required: + - source + type: object + status: + description: |- + status contains information about the state of the ClusterCatalog such as: + - Whether or not the catalog contents are being served via the catalog content HTTP server + - Whether or not the ClusterCatalog is progressing to a new state + - A reference to the source from which the catalog contents were retrieved + properties: + conditions: + description: |- + conditions is a representation of the current state for this ClusterCatalog. + + The current condition types are Serving and Progressing. + + The Serving condition is used to represent whether or not the contents of the catalog is being served via the HTTP(S) web server. + When it has a status of True and a reason of Available, the contents of the catalog are being served. + When it has a status of False and a reason of Unavailable, the contents of the catalog are not being served because the contents are not yet available. + When it has a status of False and a reason of UserSpecifiedUnavailable, the contents of the catalog are not being served because the catalog has been intentionally marked as unavailable. + + The Progressing condition is used to represent whether or not the ClusterCatalog is progressing or is ready to progress towards a new state. + When it has a status of True and a reason of Retrying, there was an error in the progression of the ClusterCatalog that may be resolved on subsequent reconciliation attempts. + When it has a status of True and a reason of Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + When it has a status of False and a reason of Blocked, there was an error in the progression of the ClusterCatalog that requires manual intervention for recovery. + + In the case that the Serving condition is True with reason Available and Progressing is True with reason Retrying, the previously fetched + catalog contents are still being served via the HTTP(S) web server while we are progressing towards serving a new version of the catalog + contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes + to the contents we identify that there are updates to the contents. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUnpacked: + description: |- + lastUnpacked represents the last time the contents of the + catalog were extracted from their source format. As an example, + when using an Image source, the OCI image will be pulled and the + image layers written to a file-system backed cache. We refer to the + act of this extraction from the source format as "unpacking". + format: date-time + type: string + resolvedSource: + description: resolvedSource contains information about the resolved + source based on the source type. + properties: + image: + description: |- + image is a field containing resolution information for a catalog sourced from an image. + This field must be set when type is Image, and forbidden otherwise. + properties: + ref: + description: |- + ref contains the resolved image digest-based reference. + The digest format is used so users can use other tooling to fetch the exact + OCI manifests that were used to extract the catalog contents. + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest + rule: self.find('(@.*:)') != "" + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", information about the resolved image source will be set in the 'image' field. + enum: + - Image + type: string + required: + - image + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + urls: + description: urls contains the URLs that can be used to access the + catalog. + properties: + base: + description: |- + base is a cluster-internal URL that provides endpoints for + accessing the content of the catalog. + + It is expected that clients append the path for the endpoint they wish + to access. + + Currently, only a single endpoint is served and is accessible at the path + /api/v1. + + The endpoints served for the v1 API are: + - /all - this endpoint returns the entirety of the catalog contents in the FBC format + + As the needs of users and clients of the evolve, new endpoints may be added. + maxLength: 525 + type: string + x-kubernetes-validations: + - message: must be a valid URL + rule: isURL(self) + - message: scheme must be either http or https + rule: 'isURL(self) ? (url(https://github.com/self).getScheme() == "http" || url(https://github.com/self).getScheme() + == "https") : true' + required: + - base + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/base/catalogd/crd/bases/olm.operatorframework.io_clustercatalogs.yaml b/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml similarity index 99% rename from config/base/catalogd/crd/bases/olm.operatorframework.io_clustercatalogs.yaml rename to helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml index 5ee98d6a3b..94f1d7121d 100644 --- a/config/base/catalogd/crd/bases/olm.operatorframework.io_clustercatalogs.yaml +++ b/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml @@ -3,7 +3,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.3 + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard name: clustercatalogs.olm.operatorframework.io spec: group: olm.operatorframework.io diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml new file mode 100644 index 0000000000..5004c8c6fd --- /dev/null +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml @@ -0,0 +1,213 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + description: |- + Phases are groups of objects that will be applied at the same time. + All objects in the phase will have to pass their probes in order to progress to the next phase. + items: + description: |- + ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. + All objects in the a phase will have to pass their probes in order to progress to the next phase. + properties: + name: + description: Name identifies this phase. + maxLength: 63 + pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ + type: string + objects: + description: Objects are a list of all the objects within this + phase. + items: + description: ClusterExtensionRevisionObject contains an object + and settings for it. + properties: + collisionProtection: + default: Prevent + description: |- + CollisionProtection controls whether OLM can adopt and modify objects + already existing on the cluster or even owned by another controller. + type: string + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + description: Previous references previous revisions that objects can + be adopted from. + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + description: |- + Revision is a sequence number representing a specific revision of the ClusterExtension instance. + Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + format: int64 + minimum: 1 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml new file mode 100644 index 0000000000..1038b7fdf0 --- /dev/null +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml @@ -0,0 +1,629 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml new file mode 100644 index 0000000000..61337bad60 --- /dev/null +++ b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml @@ -0,0 +1,629 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/olmv1/templates/_helpers.tpl b/helm/olmv1/templates/_helpers.tpl new file mode 100644 index 0000000000..89cb398934 --- /dev/null +++ b/helm/olmv1/templates/_helpers.tpl @@ -0,0 +1,65 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "olmv1.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "olmv1.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Return the name of the active component for a prefix, but _only_ if one is enabled +*/}} +{{- define "component.name.prefix" -}} +{{- if and (.Values.options.operatorController.enabled) (not .Values.options.catalogd.enabled) -}} +operator-controller- +{{- else if and (not .Values.options.operatorController.enabled) (.Values.options.catalogd.enabled) -}} +catalogd- +{{- end -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "olmv1.labels" -}} +app.kubernetes.io/part-of: olm +{{- end }} + +{{/* +Common annoations +*/}} +{{- define "olmv1.annotations" -}} +olm.operatorframework.io/feature-set: {{ .Values.options.featureSet -}}{{- if .Values.options.e2e.enabled -}}-e2e{{- end -}} +{{- end }} + +{{/* +Insertion of additional rules for RBAC +*/}} + +{{/* +Returns "operator-controller", "catalogd" or "olmv1" depending on enabled components +*/}} +{{- define "olmv1.label.name" -}} +{{- if (and .Values.options.operatorController.enabled (not .Values.options.catalogd.enabled)) -}} +operator-controller +{{- else if (and (not .Values.options.operatorController.enabled) .Values.options.catalogd.enabled) -}} +catalogd +{{- else -}} +olmv1 +{{- end -}} +{{- end -}} + +{{/* +When rendering with OpenShift, only one of the main components (catalogd, operatorController) +should be enabled +*/}} +{{- if .Values.options.openshift.enabled -}} +{{- if and .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +{{- fail "When rendering Openshift, only one of {catalogd, operatorController} should also be enabled" -}} +{{- end -}} +{{- end -}} diff --git a/config/components/tls/ca/issuers.yaml b/helm/olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml similarity index 52% rename from config/components/tls/ca/issuers.yaml rename to helm/olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml index 00e149d56b..7b3c2396a1 100644 --- a/config/components/tls/ca/issuers.yaml +++ b/helm/olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml @@ -1,35 +1,27 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: self-sign-issuer - namespace: cert-manager -spec: - selfSigned: {} ---- +{{- if .Values.options.certManager.enabled }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} name: olmv1-ca - namespace: cert-manager + namespace: {{ .Values.namespaces.certManager.name }} spec: - isCA: true commonName: olmv1-ca + isCA: true + issuerRef: + group: cert-manager.io + kind: Issuer + name: self-sign-issuer + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 secretName: olmv1-ca secretTemplate: annotations: cert-manager.io/allow-direct-injection: "true" - privateKey: - algorithm: ECDSA - size: 256 - issuerRef: - name: self-sign-issuer - kind: Issuer - group: cert-manager.io ---- -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - name: olmv1-ca -spec: - ca: - secretName: olmv1-ca +{{- end }} diff --git a/helm/olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml b/helm/olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml new file mode 100644 index 0000000000..7c6311eedf --- /dev/null +++ b/helm/olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml @@ -0,0 +1,26 @@ +{{- if .Values.options.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + name: catalogd-service-cert + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + dnsNames: + - localhost + - catalogd-service.{{ .Values.namespaces.olmv1.name }}.svc + - catalogd-service.{{ .Values.namespaces.olmv1.name }}.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: catalogd-service-cert-git-version +{{- end }} diff --git a/helm/olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml b/helm/olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml new file mode 100644 index 0000000000..2ac8371935 --- /dev/null +++ b/helm/olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml @@ -0,0 +1,25 @@ +{{- if .Values.options.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: operator-controller-cert + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + dnsNames: + - operator-controller-service.{{ .Values.namespaces.olmv1.name }}.svc + - operator-controller-service.{{ .Values.namespaces.olmv1.name }}.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: operator-controller-cert +{{- end }} diff --git a/helm/olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml b/helm/olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml new file mode 100644 index 0000000000..57573095fd --- /dev/null +++ b/helm/olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml @@ -0,0 +1,14 @@ +{{- if .Values.options.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: olmv1-ca +spec: + ca: + secretName: olmv1-ca +{{- end }} diff --git a/helm/olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml b/helm/olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml new file mode 100644 index 0000000000..283e62c266 --- /dev/null +++ b/helm/olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml @@ -0,0 +1,14 @@ +{{- if .Values.options.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: self-sign-issuer + namespace: {{ .Values.namespaces.certManager.name }} +spec: + selfSigned: {} +{{- end }} diff --git a/helm/olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml b/helm/olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml new file mode 100644 index 0000000000..5414b93f50 --- /dev/null +++ b/helm/olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml @@ -0,0 +1,9 @@ +{{- if .Values.options.catalogd.enabled }} +{{- if (eq .Values.options.featureSet "standard") }} +{{ tpl (.Files.Get "base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml") . }} +{{- else if (eq .Values.options.featureSet "experimental") }} +{{ tpl (.Files.Get "base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml") . }} +{{- else }} +{{- fail "options.featureSet must be set to one of: {standard,experimental}" }} +{{- end }} +{{- end }} diff --git a/helm/olmv1/templates/crds/customresourcedefinition-clusterextensionrevisions.olm.operatorframework.io.yml b/helm/olmv1/templates/crds/customresourcedefinition-clusterextensionrevisions.olm.operatorframework.io.yml new file mode 100644 index 0000000000..c006ed20f7 --- /dev/null +++ b/helm/olmv1/templates/crds/customresourcedefinition-clusterextensionrevisions.olm.operatorframework.io.yml @@ -0,0 +1,9 @@ +{{- if .Values.options.operatorController.enabled }} +{{- if (eq .Values.options.featureSet "standard") }} +{{- /* Add when GA: tpl (.Files.Get "base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensionrevisionss.yaml") . */}} +{{- else if (eq .Values.options.featureSet "experimental") }} +{{ tpl (.Files.Get "base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensionrevisions.yaml") . }} +{{- else }} +{{- fail "options.featureSet must be set to one of: {standard,experimental}" }} +{{- end }} +{{- end }} diff --git a/helm/olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml b/helm/olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml new file mode 100644 index 0000000000..56e878104f --- /dev/null +++ b/helm/olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml @@ -0,0 +1,9 @@ +{{- if .Values.options.operatorController.enabled }} +{{- if (eq .Values.options.featureSet "standard") }} +{{ tpl (.Files.Get "base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml") . }} +{{- else if (eq .Values.options.featureSet "experimental") }} +{{ tpl (.Files.Get "base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml") . }} +{{- else }} +{{- fail "options.featureSet must be set to one of: {standard,experimental}" }} +{{- end }} +{{- end }} diff --git a/helm/olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml b/helm/olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml new file mode 100644 index 0000000000..b3df12139c --- /dev/null +++ b/helm/olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml @@ -0,0 +1,203 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + name: catalogd-controller-manager + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + minReadySeconds: 5 + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: catalogd-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- include "olmv1.annotations" . | nindent 8 }} + {{- if .Values.options.openshift.enabled }} + target.workload.openshift.io/management: '{"effect": "PreferredDuringScheduling"}' + openshift.io/required-scc: privileged + {{- end }} + labels: + app.kubernetes.io/name: catalogd + control-plane: catalogd-controller-manager + {{- include "olmv1.labels" . | nindent 8 }} + {{- with .Values.options.catalogd.deployment.podLabels }} + {{- toYamlPretty . | nindent 8 }} + {{- end }} + spec: + containers: + - args: + {{- if not .Values.options.tilt.enabled }} + - --leader-elect + {{- end }} + - --metrics-bind-address=:7443 + - --external-address=catalogd-service.{{ .Values.namespaces.olmv1.name }}.svc + {{- range .Values.options.catalogd.features.enabled }} + - --feature-gates={{- . -}}=true + {{- end }} + {{- range .Values.options.catalogd.features.disabled }} + - --feature-gates={{- . -}}=false + {{- end }} + {{- range .Values.options.catalogd.deployment.extraArguments }} + - {{ . -}} + {{- end }} + {{- if .Values.options.certManager.enabled }} + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --pull-cas-dir=/var/ca-certs + {{- else if .Values.options.openshift.enabled }} + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --v=${LOG_VERBOSITY} + - --global-pull-secret=openshift-config/pull-secret + {{- end }} + {{- if .Values.options.e2e.enabled }} + {{- /* This is effectively modern with the CHACHA cipher and secp384r1 curve removed */}} + - --tls-profile=custom + - --tls-custom-version=TLSv1.3 + - --tls-custom-curves=X25519,prime256v1 + - --tls-custom-ciphers=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384 + {{- end }} + command: + - ./catalogd + {{- if or .Values.options.e2e.enabled .Values.options.openshift.enabled }} + env: + {{- if .Values.options.e2e.enabled }} + - name: GOCOVERDIR + value: /e2e-coverage + {{- end }} + {{- if .Values.options.openshift.enabled }} + - name: SSL_CERT_DIR + value: /var/ca-certs + {{- end }} + {{- end }} + image: "{{ .Values.options.catalogd.deployment.image }}" + name: manager + {{- if not .Values.options.tilt.enabled }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + {{- end }} + resources: + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + {{- if .Values.options.e2e.enabled }} + - mountPath: /e2e-coverage + name: e2e-coverage-volume + {{- end }} + - mountPath: /var/cache/ + name: cache + - mountPath: /tmp + name: tmp + {{- if .Values.options.certManager.enabled }} + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + {{- else if .Values.options.openshift.enabled }} + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + - mountPath: /etc/containers + name: etc-containers + readOnly: true + - mountPath: /etc/docker + name: etc-docker + readOnly: true + {{- end }} + {{- with .Values.deployments.containerSpec }} + {{- toYamlPretty . | nindent 10 }} + {{- end }} + serviceAccountName: catalogd-controller-manager + volumes: + {{- if .Values.options.e2e.enabled }} + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + {{- end }} + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + {{- if .Values.options.certManager.enabled }} + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogd-service-cert-git-version + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: catalogd-service-cert-git-version + {{- else if .Values.options.openshift.enabled }} + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogserver-cert + - name: ca-certs + projected: + sources: + - configMap: + items: + - key: ca-bundle.crt + path: ca-bundle.crt + name: catalogd-trusted-ca-bundle + optional: false + - configMap: + items: + - key: service-ca.crt + path: service-ca.crt + name: openshift-service-ca.crt + optional: false + - hostPath: + path: /etc/containers + type: Directory + name: etc-containers + - hostPath: + path: /etc/docker + type: Directory + name: etc-docker + {{- end }} + {{- with .Values.deployments.templateSpec }} + {{- toYamlPretty . | nindent 6 }} + {{- end }} +{{- end }} diff --git a/helm/olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml b/helm/olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml new file mode 100644 index 0000000000..9ec405a3e0 --- /dev/null +++ b/helm/olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml @@ -0,0 +1,206 @@ +{{- if .Values.options.operatorController.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + name: operator-controller-controller-manager + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: operator-controller-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- include "olmv1.annotations" . | nindent 8 }} + {{- if .Values.options.openshift.enabled }} + target.workload.openshift.io/management: '{"effect": "PreferredDuringScheduling"}' + openshift.io/required-scc: privileged + {{- end }} + labels: + app.kubernetes.io/name: operator-controller + control-plane: operator-controller-controller-manager + {{- include "olmv1.labels" . | nindent 8 }} + {{- with .Values.options.operatorController.deployment.podLabels }} + {{- toYamlPretty . | nindent 8 }} + {{- end }} + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8443 + {{- if not .Values.options.tilt.enabled }} + - --leader-elect + {{- end }} + {{- range .Values.options.operatorController.features.enabled }} + - --feature-gates={{- . -}}=true + {{- end }} + {{- range .Values.options.operatorController.features.disabled }} + - --feature-gates={{- . -}}=false + {{- end }} + {{- range .Values.options.operatorController.deployment.extraArguments }} + - {{ . -}} + {{- end }} + {{- if .Values.options.certManager.enabled }} + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --pull-cas-dir=/var/ca-certs + {{- else if .Values.options.openshift.enabled }} + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --v=${LOG_VERBOSITY} + - --global-pull-secret=openshift-config/pull-secret + {{- end }} + {{- if .Values.options.e2e.enabled }} + - --tls-profile=modern + {{- end }} + command: + - /operator-controller + {{- if or .Values.options.e2e.enabled .Values.options.openshift.enabled }} + env: + {{- if .Values.options.e2e.enabled }} + - name: GOCOVERDIR + value: /e2e-coverage + {{- end }} + {{- if .Values.options.openshift.enabled }} + - name: SSL_CERT_DIR + value: /var/ca-certs + {{- end }} + {{- end }} + image: "{{ .Values.options.operatorController.deployment.image }}" + name: manager + {{- if not .Values.options.tilt.enabled }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + {{- end }} + resources: + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + {{- if .Values.options.e2e.enabled }} + - mountPath: /etc/containers + name: e2e-registries-conf + - mountPath: /e2e-coverage + name: e2e-coverage-volume + {{- end }} + - mountPath: /var/cache + name: cache + - mountPath: /tmp + name: tmp + {{- if .Values.options.certManager.enabled }} + - mountPath: /var/certs + name: operator-controller-certs + readOnly: true + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + {{- else if .Values.options.openshift.enabled }} + - mountPath: /var/certs + name: operator-controller-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + - mountPath: /etc/containers + name: etc-containers + readOnly: true + - mountPath: /etc/docker + name: etc-docker + readOnly: true + {{- end }} + {{- with .Values.deployments.containerSpec }} + {{- toYaml . | nindent 10 }} + {{- end }} + serviceAccountName: operator-controller-controller-manager + volumes: + {{- if .Values.options.e2e.enabled }} + - configMap: + name: e2e-registries-conf + name: e2e-registries-conf + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + {{- end }} + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + {{- if .Values.options.certManager.enabled }} + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: operator-controller-cert + {{- else if .Values.options.openshift.enabled }} + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + projected: + sources: + - configMap: + items: + - key: ca-bundle.crt + path: ca-bundle.crt + name: operator-controller-trusted-ca-bundle + optional: false + - configMap: + items: + - key: service-ca.crt + path: service-ca.crt + name: openshift-service-ca.crt + optional: false + - hostPath: + path: /etc/containers + type: Directory + name: etc-containers + - hostPath: + path: /etc/docker + type: Directory + name: etc-docker + {{- end }} + {{- with .Values.deployments.templateSpec }} + {{- toYamlPretty . | nindent 6 }} + {{- end }} +{{- end }} diff --git a/config/components/registries-conf/registries_conf_configmap.yaml b/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml similarity index 51% rename from config/components/registries-conf/registries_conf_configmap.yaml rename to helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml index 2604c78f56..d6fec9b5fb 100644 --- a/config/components/registries-conf/registries_conf_configmap.yaml +++ b/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml @@ -1,10 +1,17 @@ +{{- if .Values.options.e2e.enabled }} apiVersion: v1 -kind: ConfigMap -metadata: - name: e2e-registries-conf - namespace: system data: registries.conf: | [[registry]] prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" +kind: ConfigMap +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: e2e + {{- include "olmv1.labels" . | nindent 4 }} + name: e2e-registries-conf + namespace: {{ .Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/e2e/persistentvolumeclaim-olmv1-system-e2e-coverage.yml b/helm/olmv1/templates/e2e/persistentvolumeclaim-olmv1-system-e2e-coverage.yml new file mode 100644 index 0000000000..6f5c83fced --- /dev/null +++ b/helm/olmv1/templates/e2e/persistentvolumeclaim-olmv1-system-e2e-coverage.yml @@ -0,0 +1,18 @@ +{{- if .Values.options.e2e.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: e2e + {{- include "olmv1.labels" . | nindent 4 }} + name: e2e-coverage + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64Mi +{{- end }} diff --git a/helm/olmv1/templates/e2e/pod-olmv1-system-e2e-coverage-copy-pod.yml b/helm/olmv1/templates/e2e/pod-olmv1-system-e2e-coverage-copy-pod.yml new file mode 100644 index 0000000000..fa4b11acaa --- /dev/null +++ b/helm/olmv1/templates/e2e/pod-olmv1-system-e2e-coverage-copy-pod.yml @@ -0,0 +1,40 @@ +{{- if .Values.options.e2e.enabled }} +apiVersion: v1 +kind: Pod +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: e2e + {{- include "olmv1.labels" . | nindent 4 }} + name: e2e-coverage-copy-pod + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + containers: + - command: + - sleep + - infinity + image: busybox:1.36 + name: tar + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /e2e-coverage + name: e2e-coverage-volume + readOnly: true + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + volumes: + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + readOnly: true +{{- end }} diff --git a/helm/olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml b/helm/olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml new file mode 100644 index 0000000000..95077c9ffe --- /dev/null +++ b/helm/olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml @@ -0,0 +1,43 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: catalogd-mutating-webhook-configuration + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + annotations: + {{- if .Values.options.certManager.enabled }} + cert-manager.io/inject-ca-from-secret: cert-manager/olmv1-ca + {{- end }} + {{- if .Values.options.openshift.enabled }} + service.beta.openshift.io/inject-cabundle: "true" + {{- end }} + {{- include "olmv1.annotations" . | nindent 4 }} +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: catalogd-service + namespace: {{ .Values.namespaces.olmv1.name }} + path: /mutate-olm-operatorframework-io-v1-clustercatalog + port: 9443 + failurePolicy: Fail + name: inject-metadata-name.olm.operatorframework.io + rules: + - apiGroups: + - olm.operatorframework.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clustercatalogs + sideEffects: None + timeoutSeconds: 10 + matchConditions: + - name: MissingOrIncorrectMetadataNameLabel + expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" +{{- end }} diff --git a/helm/olmv1/templates/namespace.yml b/helm/olmv1/templates/namespace.yml new file mode 100644 index 0000000000..4624909d9d --- /dev/null +++ b/helm/olmv1/templates/namespace.yml @@ -0,0 +1,24 @@ +{{/* this is a common component */}} +apiVersion: v1 +kind: Namespace +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + {{- if .Values.options.openshift.enabled }} + openshift.io/node-selector: "" + workload.openshift.io/allowed: management + {{- end }} + labels: + {{- $psProfile := ternary "privileged" "restricted" .Values.options.openshift.enabled }} + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + pod-security.kubernetes.io/audit: {{ $psProfile }} + pod-security.kubernetes.io/audit-version: latest + pod-security.kubernetes.io/enforce: {{ $psProfile }} + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: {{ $psProfile }} + pod-security.kubernetes.io/warn-version: latest + {{- include "olmv1.labels" . | nindent 4 }} + {{- if .Values.options.openshift.enabled }} + openshift.io/cluster-monitoring: "true" + {{- end }} + name: {{ .Values.namespaces.olmv1.name }} diff --git a/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml new file mode 100644 index 0000000000..803e2c5943 --- /dev/null +++ b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml @@ -0,0 +1,29 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + name: catalogd-controller-manager + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + egress: + - {} + ingress: + - ports: + - port: 7443 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 9443 + protocol: TCP + podSelector: + matchLabels: + control-plane: catalogd-controller-manager + policyTypes: + - Ingress + - Egress +{{- end }} diff --git a/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml new file mode 100644 index 0000000000..e39a84a880 --- /dev/null +++ b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml @@ -0,0 +1,16 @@ +{{/* this is a common component */}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "component.name.prefix" . -}}default-deny-all-traffic + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress diff --git a/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml new file mode 100644 index 0000000000..fc85c57b84 --- /dev/null +++ b/helm/olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml @@ -0,0 +1,25 @@ +{{- if .Values.options.operatorController.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + name: operator-controller-controller-manager + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + control-plane: operator-controller-controller-manager + policyTypes: + - Ingress + - Egress +{{- end }} diff --git a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-certified-operators.yml b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-certified-operators.yml new file mode 100644 index 0000000000..86c92fd55d --- /dev/null +++ b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-certified-operators.yml @@ -0,0 +1,13 @@ +{{- if and .Values.options.openshift.enabled .Values.options.catalogd.enabled -}} +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: openshift-certified-operators +spec: + priority: -200 + source: + type: Image + image: + pollIntervalMinutes: 10 + ref: registry.redhat.io/redhat/certified-operator-index:{{- .Values.options.openshift.catalogs.version }} +{{- end -}} diff --git a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-community-operators.yml b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-community-operators.yml new file mode 100644 index 0000000000..529af4ad51 --- /dev/null +++ b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-community-operators.yml @@ -0,0 +1,13 @@ +{{- if and .Values.options.openshift.enabled .Values.options.catalogd.enabled -}} +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: openshift-community-operators +spec: + priority: -400 + source: + type: Image + image: + pollIntervalMinutes: 10 + ref: registry.redhat.io/redhat/community-operator-index:{{- .Values.options.openshift.catalogs.version }} +{{- end -}} diff --git a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml new file mode 100644 index 0000000000..b8d6bcff48 --- /dev/null +++ b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml @@ -0,0 +1,13 @@ +{{- if and .Values.options.openshift.enabled .Values.options.catalogd.enabled -}} +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: openshift-redhat-marketplace +spec: + priority: -300 + source: + type: Image + image: + pollIntervalMinutes: 10 + ref: registry.redhat.io/redhat/redhat-marketplace-index:{{- .Values.options.openshift.catalogs.version }} +{{- end -}} diff --git a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-operators.yml b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-operators.yml new file mode 100644 index 0000000000..d7d94dac11 --- /dev/null +++ b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-operators.yml @@ -0,0 +1,13 @@ +{{- if and .Values.options.openshift.enabled .Values.options.catalogd.enabled -}} +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: openshift-redhat-operators +spec: + priority: -100 + source: + type: Image + image: + pollIntervalMinutes: 10 + ref: registry.redhat.io/redhat/redhat-operator-index:{{ .Values.options.openshift.catalogs.version }} +{{- end -}} diff --git a/helm/olmv1/templates/openshift/configmap-trusted-ca.yml b/helm/olmv1/templates/openshift/configmap-trusted-ca.yml new file mode 100644 index 0000000000..b5fcf93138 --- /dev/null +++ b/helm/olmv1/templates/openshift/configmap-trusted-ca.yml @@ -0,0 +1,15 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + config.openshift.io/inject-trusted-cabundle: "true" + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-trusted-ca-bundle + namespace: {{ .Values.namespaces.olmv1.name }} +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/templates/openshift/role-openshift-config-manager-role.yml b/helm/olmv1/templates/openshift/role-openshift-config-manager-role.yml new file mode 100644 index 0000000000..0a708152fc --- /dev/null +++ b/helm/olmv1/templates/openshift/role-openshift-config-manager-role.yml @@ -0,0 +1,23 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-manager-role + namespace: openshift-config +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/templates/openshift/rolebinding-openshift-config-manager-rolebinding.yml b/helm/olmv1/templates/openshift/rolebinding-openshift-config-manager-rolebinding.yml new file mode 100644 index 0000000000..2209f5c579 --- /dev/null +++ b/helm/olmv1/templates/openshift/rolebinding-openshift-config-manager-rolebinding.yml @@ -0,0 +1,22 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-manager-rolebinding + namespace: openshift-config +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "olmv1.label.name" . -}}-manager-role +subjects: +- kind: ServiceAccount + name: {{ include "olmv1.label.name" . -}}-controller-manager + namespace: {{ .Values.namespaces.olmv1.name }} +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml b/helm/olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml new file mode 100644 index 0000000000..fe43d1966f --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml @@ -0,0 +1,48 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: catalogd-manager-role + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/status + verbs: + - get + - patch + - update + {{- if .Values.options.openshift.enabled }} + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use + {{- end }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrole-common-metrics-reader.yml b/helm/olmv1/templates/rbac/clusterrole-common-metrics-reader.yml new file mode 100644 index 0000000000..069041955d --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrole-common-metrics-reader.yml @@ -0,0 +1,24 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $| nindent 4 }} + name: {{ $name -}}-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrole-common-proxy-role.yml b/helm/olmv1/templates/rbac/clusterrole-common-proxy-role.yml new file mode 100644 index 0000000000..266348e2f3 --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrole-common-proxy-role.yml @@ -0,0 +1,32 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml b/helm/olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml new file mode 100644 index 0000000000..9cd843b51d --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml @@ -0,0 +1,21 @@ +{{- if .Values.options.operatorController.enabled }} +{{/* Probably want to include this as a file somehow */}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + name: operator-controller-clusterextension-viewer-role +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - watch +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml b/helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml new file mode 100644 index 0000000000..84f221003c --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml @@ -0,0 +1,75 @@ +{{- if and .Values.options.operatorController.enabled (not (has "BoxcutterRuntime" .Values.operatorConrollerFeatures)) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-controller-manager-role + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch + {{- if .Values.options.openshift.enabled }} + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use + {{- end }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml b/helm/olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml new file mode 100644 index 0000000000..3c5f0daf4a --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml @@ -0,0 +1,20 @@ +{{- if .Values.options.catalogd.enabled }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" $ | nindent 4 }} + name: catalogd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml b/helm/olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml new file mode 100644 index 0000000000..b53096f139 --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml @@ -0,0 +1,27 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ $name -}}-proxy-role +subjects: + - kind: ServiceAccount + name: {{ $name -}}-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml b/helm/olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml new file mode 100644 index 0000000000..9817337dff --- /dev/null +++ b/helm/olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml @@ -0,0 +1,28 @@ +{{- if .Values.options.operatorController.enabled }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" $ | nindent 4 }} +{{- if has "BoxcutterRuntime" .Values.options.operatorController.features.enabled }} + name: operator-controller-manager-admin-rolebinding +{{- else }} + name: operator-controller-manager-rolebinding +{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole +{{- if has "BoxcutterRuntime" .Values.options.operatorController.features.enabled }} + name: cluster-admin +{{- else }} + name: operator-controller-manager-role +{{- end }} +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml b/helm/olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml new file mode 100644 index 0000000000..09cec7c0c2 --- /dev/null +++ b/helm/olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml @@ -0,0 +1,22 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: catalogd-manager-role + namespace: {{ .Values.namespaces.olmv1.name }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +{{- end }} diff --git a/helm/olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml b/helm/olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml new file mode 100644 index 0000000000..41c1c7bb73 --- /dev/null +++ b/helm/olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml @@ -0,0 +1,40 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-leader-election-role + namespace: {{ $.Values.namespaces.olmv1.name }} +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +{{- end }} diff --git a/helm/olmv1/templates/rbac/role-olmv1-system-metrics-monitor-role.yml b/helm/olmv1/templates/rbac/role-olmv1-system-metrics-monitor-role.yml new file mode 100644 index 0000000000..0a452d6b90 --- /dev/null +++ b/helm/olmv1/templates/rbac/role-olmv1-system-metrics-monitor-role.yml @@ -0,0 +1,25 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-metrics-monitor-role + namespace: {{ .Values.namespaces.olmv1.name }} +rules: + - apiGroups: + - "" + resources: + - services + - endpoints + - pods + verbs: + - get + - list + - watch +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml b/helm/olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml new file mode 100644 index 0000000000..2e31957d30 --- /dev/null +++ b/helm/olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml @@ -0,0 +1,34 @@ +{{- if .Values.options.operatorController.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: operator-controller-manager-role + namespace: {{ .Values.namespaces.olmv1.name }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +{{- end }} diff --git a/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml new file mode 100644 index 0000000000..d8ab8f1178 --- /dev/null +++ b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml @@ -0,0 +1,28 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-leader-election-rolebinding + namespace: {{ $.Values.namespaces.olmv1.name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ $name -}}-leader-election-role +subjects: + - kind: ServiceAccount + name: {{ $name -}}-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml new file mode 100644 index 0000000000..a8846104ab --- /dev/null +++ b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml @@ -0,0 +1,28 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-manager-rolebinding + namespace: {{ $.Values.namespaces.olmv1.name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ $name -}}-manager-role +subjects: + - kind: ServiceAccount + name: {{ $name -}}-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/rbac/rolebinding-olmv1-system-metrics-monitor-rolebinding.yml b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-metrics-monitor-rolebinding.yml new file mode 100644 index 0000000000..18ec318a2a --- /dev/null +++ b/helm/olmv1/templates/rbac/rolebinding-olmv1-system-metrics-monitor-rolebinding.yml @@ -0,0 +1,22 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-metrics-monitor-rolebinding + namespace: {{ .Values.namespaces.olmv1.name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "olmv1.label.name" . -}}-metrics-monitor-role +subjects: + - kind: ServiceAccount + name: prometheus-k8s + namespace: openshift-monitoring +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/templates/service-olmv1-system-catalogd-service.yml b/helm/olmv1/templates/service-olmv1-system-catalogd-service.yml new file mode 100644 index 0000000000..eca9593995 --- /dev/null +++ b/helm/olmv1/templates/service-olmv1-system-catalogd-service.yml @@ -0,0 +1,31 @@ +{{- if .Values.options.catalogd.enabled }} +apiVersion: v1 +kind: Service +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + {{- if .Values.options.openshift.enabled }} + service.beta.openshift.io/serving-cert-secret-name: catalogserver-cert + {{- end }} + labels: + app.kubernetes.io/name: catalogd + {{- include "olmv1.labels" . | nindent 4 }} + name: catalogd-service + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + - name: webhook + port: 9443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 7443 + protocol: TCP + targetPort: 7443 + selector: + app.kubernetes.io/name: catalogd +{{- end }} diff --git a/helm/olmv1/templates/service-olmv1-system-operator-controller-service.yml b/helm/olmv1/templates/service-olmv1-system-operator-controller-service.yml new file mode 100644 index 0000000000..714894f4e5 --- /dev/null +++ b/helm/olmv1/templates/service-olmv1-system-operator-controller-service.yml @@ -0,0 +1,23 @@ +{{- if .Values.options.operatorController.enabled }} +apiVersion: v1 +kind: Service +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + {{- if .Values.options.openshift.enabled }} + service.beta.openshift.io/serving-cert-secret-name: operator-controller-cert + {{- end }} + labels: + app.kubernetes.io/name: operator-controller + {{- include "olmv1.labels" . | nindent 4 }} + name: operator-controller-service + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + ports: + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: operator-controller +{{- end }} diff --git a/helm/olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml b/helm/olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml new file mode 100644 index 0000000000..f29464ede3 --- /dev/null +++ b/helm/olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml @@ -0,0 +1,20 @@ +{{- $options := list }} +{{- if .Values.options.catalogd.enabled }} +{{- $options = append $options "catalogd" }} +{{- end }} +{{- if .Values.options.operatorController.enabled }} +{{- $options = append $options "operator-controller" }} +{{- end }} +{{- range $index, $name := $options }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + {{- include "olmv1.annotations" $ | nindent 4 }} + labels: + app.kubernetes.io/name: {{ $name }} + {{- include "olmv1.labels" $ | nindent 4 }} + name: {{ $name -}}-controller-manager + namespace: {{ $.Values.namespaces.olmv1.name }} +{{- end }} diff --git a/helm/olmv1/templates/servicemonitor-olmv1-system-metrics-monitor.yml b/helm/olmv1/templates/servicemonitor-olmv1-system-metrics-monitor.yml new file mode 100644 index 0000000000..a5bb357c37 --- /dev/null +++ b/helm/olmv1/templates/servicemonitor-olmv1-system-metrics-monitor.yml @@ -0,0 +1,33 @@ +{{- if .Values.options.openshift.enabled -}} +{{- if or .Values.options.catalogd.enabled .Values.options.operatorController.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + annotations: + {{- include "olmv1.annotations" . | nindent 4 }} + labels: + openshift.io/cluster-monitoring: 'true' + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} + {{- include "olmv1.labels" . | nindent 4 }} + name: {{ include "olmv1.label.name" . -}}-metrics-monitor + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + interval: 30s + path: /metrics + port: metrics + scheme: https + tlsConfig: + caFile: /etc/prometheus/configmaps/serving-certs-ca-bundle/service-ca.crt + certFile: /etc/prometheus/secrets/metrics-client-certs/tls.crt + keyFile: /etc/prometheus/secrets/metrics-client-certs/tls.key + serverName: {{ include "olmv1.label.name" . -}}-service.{{ .Values.namespaces.olmv1.name }}.svc + namespaceSelector: + matchNames: + - {{ .Values.namespaces.olmv1.name }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "olmv1.label.name" . }} +{{- end -}} +{{- end -}} diff --git a/helm/olmv1/values.yaml b/helm/olmv1/values.yaml new file mode 100644 index 0000000000..0704f43ef3 --- /dev/null +++ b/helm/olmv1/values.yaml @@ -0,0 +1,90 @@ +# Default values for OLMv1. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# List of components to include +options: + operatorController: + enabled: true + deployment: + image: quay.io/operator-framework/operator-controller:devel + extraArguments: [] + features: + enabled: [] + disabled: [] + catalogd: + enabled: true + deployment: + image: quay.io/operator-framework/catalogd:devel + extraArguments: [] + features: + enabled: [] + disabled: [] + certManager: + enabled: false + e2e: + enabled: false + tilt: + enabled: false + openshift: + enabled: false + catalogs: + version: v4.20 + # This can be one of: standard or experimental + featureSet: standard + +# The set of namespaces +namespaces: + olmv1: + name: olmv1-system + certManager: + name: cert-manager + +# Common deployment values for operator-controller and catalogd +deployments: + templateSpec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 + containerSpec: + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError diff --git a/helm/prometheus/Chart.yaml b/helm/prometheus/Chart.yaml new file mode 100644 index 0000000000..1cd44e76c5 --- /dev/null +++ b/helm/prometheus/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: prometheus +description: A Helm chart of Prometheus resources for OLMv1 +icon: https://raw.githubusercontent.com/operator-framework/operator-framework.io/refs/heads/master/static/tile70x70.png + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 diff --git a/helm/prometheus/templates/clusterrole-prometheus.yml b/helm/prometheus/templates/clusterrole-prometheus.yml new file mode 100644 index 0000000000..d109c2660a --- /dev/null +++ b/helm/prometheus/templates/clusterrole-prometheus.yml @@ -0,0 +1,44 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus +rules: + - apiGroups: + - "" + resources: + - nodes + - nodes/metrics + - services + - endpoints + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - nonResourceURLs: + - /metrics + verbs: + - get diff --git a/config/base/operator-controller/rbac/auth_proxy_role_binding.yaml b/helm/prometheus/templates/clusterrolebinding-prometheus.yml similarity index 52% rename from config/base/operator-controller/rbac/auth_proxy_role_binding.yaml rename to helm/prometheus/templates/clusterrolebinding-prometheus.yml index ec7acc0a1b..eb5b43547c 100644 --- a/config/base/operator-controller/rbac/auth_proxy_role_binding.yaml +++ b/helm/prometheus/templates/clusterrolebinding-prometheus.yml @@ -1,12 +1,13 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: proxy-rolebinding + name: prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: proxy-role + name: prometheus subjects: -- kind: ServiceAccount - name: controller-manager - namespace: system + - kind: ServiceAccount + name: prometheus + namespace: {{ .Values.namespaces.olmv1.name }} diff --git a/helm/prometheus/templates/networkpolicy-prometheus.yml b/helm/prometheus/templates/networkpolicy-prometheus.yml new file mode 100644 index 0000000000..821e7054b2 --- /dev/null +++ b/helm/prometheus/templates/networkpolicy-prometheus.yml @@ -0,0 +1,17 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: prometheus + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + egress: + - {} + ingress: + - {} + podSelector: + matchLabels: + app.kubernetes.io/name: prometheus + policyTypes: + - Egress + - Ingress diff --git a/helm/prometheus/templates/prometheus-prometheus.yml b/helm/prometheus/templates/prometheus-prometheus.yml new file mode 100644 index 0000000000..3b9df82d1f --- /dev/null +++ b/helm/prometheus/templates/prometheus-prometheus.yml @@ -0,0 +1,19 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: Prometheus +metadata: + name: prometheus + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + logLevel: debug + ruleSelector: {} + scrapeInterval: 1m + scrapeTimeout: 30s + securityContext: + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault + serviceAccountName: prometheus + serviceDiscoveryRole: EndpointSlice + serviceMonitorSelector: {} diff --git a/helm/prometheus/templates/prometheusrile-controller-alerts.yml b/helm/prometheus/templates/prometheusrile-controller-alerts.yml new file mode 100644 index 0000000000..bce2706eea --- /dev/null +++ b/helm/prometheus/templates/prometheusrile-controller-alerts.yml @@ -0,0 +1,72 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: controller-alerts + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + groups: + - name: controller-panic + rules: + - alert: reconciler-panic + annotations: + description: controller of pod {{`{{ $labels.pod }}`}} experienced panic(s); count={{`{{ $value }}`}} + expr: controller_runtime_reconcile_panics_total{} > 0 + - alert: webhook-panic + annotations: + description: controller webhook of pod {{`{{ $labels.pod }}`}} experienced panic(s); count={{`{{ $value }}`}} + expr: controller_runtime_webhook_panics_total{} > 0 + - name: resource-usage + rules: + - alert: oom-events + annotations: + description: container {{`{{ $labels.container }}`}} of pod {{`{{ $labels.pod }}`}} experienced OOM event(s); count={{`{{ $value }}`}} + expr: container_oom_events_total > 0 + - alert: operator-controller-memory-growth + annotations: + description: 'operator-controller pod memory usage growing at a high rate for 5 minutes: {{`{{ $value | humanize }}`}}B/sec' + expr: deriv(sum(container_memory_working_set_bytes{pod=~"operator-controller.*",container="manager"})[5m:]) > 100_000 + for: 5m + keep_firing_for: 1d + - alert: catalogd-memory-growth + annotations: + description: 'catalogd pod memory usage growing at a high rate for 5 minutes: {{`{{ $value | humanize }}`}}B/sec' + expr: deriv(sum(container_memory_working_set_bytes{pod=~"catalogd.*",container="manager"})[5m:]) > 100_000 + for: 5m + keep_firing_for: 1d + - alert: operator-controller-memory-usage + annotations: + description: 'operator-controller pod using high memory resources for the last 5 minutes: {{`{{ $value | humanize }}`}}B' + expr: sum(container_memory_working_set_bytes{pod=~"operator-controller.*",container="manager"}) > 100_000_000 + for: 5m + keep_firing_for: 1d + - alert: catalogd-memory-usage + annotations: + description: 'catalogd pod using high memory resources for the last 5 minutes: {{`{{ $value | humanize }}`}}B' + expr: sum(container_memory_working_set_bytes{pod=~"catalogd.*",container="manager"}) > 75_000_000 + for: 5m + keep_firing_for: 1d + - alert: operator-controller-cpu-usage + annotations: + description: 'operator-controller using high cpu resource for 5 minutes: {{`{{ $value | printf "%.2f" }}`}}%' + expr: rate(container_cpu_usage_seconds_total{pod=~"operator-controller.*",container="manager"}[5m]) * 100 > 20 + for: 5m + keep_firing_for: 1d + - alert: catalogd-cpu-usage + annotations: + description: 'catalogd using high cpu resources for 5 minutes: {{`{{ $value | printf "%.2f" }}`}}%' + expr: rate(container_cpu_usage_seconds_total{pod=~"catalogd.*",container="manager"}[5m]) * 100 > 20 + for: 5m + keep_firing_for: 1d + - alert: operator-controller-api-call-rate + annotations: + description: 'operator-controller making excessive API calls for 5 minutes: {{`{{ $value | printf "%.2f" }}`}}/sec' + expr: sum(rate(rest_client_requests_total{job=~"operator-controller-service"}[5m])) > 10 + for: 5m + keep_firing_for: 1d + - alert: catalogd-api-call-rate + annotations: + description: 'catalogd making excessive API calls for 5 minutes: {{`{{ $value | printf "%.2f" }}`}}/sec' + expr: sum(rate(rest_client_requests_total{job=~"catalogd-service"}[5m])) > 5 + for: 5m + keep_firing_for: 1d diff --git a/helm/prometheus/templates/secret-prometheus-metrics-token.yml b/helm/prometheus/templates/secret-prometheus-metrics-token.yml new file mode 100644 index 0000000000..9db86fe995 --- /dev/null +++ b/helm/prometheus/templates/secret-prometheus-metrics-token.yml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + annotations: + kubernetes.io/service-account.name: prometheus + name: prometheus-metrics-token + namespace: {{ .Values.namespaces.olmv1.name }} +type: kubernetes.io/service-account-token diff --git a/helm/prometheus/templates/service-prometheus-service.yml b/helm/prometheus/templates/service-prometheus-service.yml new file mode 100644 index 0000000000..11c0555104 --- /dev/null +++ b/helm/prometheus/templates/service-prometheus-service.yml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus-service + namespace: {{ .Values.namespaces.prometheus.name }} +spec: + ports: + - name: web + nodePort: 30900 + port: 9090 + protocol: TCP + targetPort: web + selector: + prometheus: prometheus + type: NodePort diff --git a/helm/prometheus/templates/serviceaccount-prometheus.yml b/helm/prometheus/templates/serviceaccount-prometheus.yml new file mode 100644 index 0000000000..da68eb7aae --- /dev/null +++ b/helm/prometheus/templates/serviceaccount-prometheus.yml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus + namespace: {{ .Values.namespaces.olmv1.name }} diff --git a/helm/prometheus/templates/servicemonitor-catalogd-controller-manager-metrics-monitor.yml b/helm/prometheus/templates/servicemonitor-catalogd-controller-manager-metrics-monitor.yml new file mode 100644 index 0000000000..b3fd498547 --- /dev/null +++ b/helm/prometheus/templates/servicemonitor-catalogd-controller-manager-metrics-monitor.yml @@ -0,0 +1,33 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: catalogd-controller-manager-metrics-monitor + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + endpoints: + - authorization: + credentials: + key: token + name: prometheus-metrics-token + interval: 10s + path: /metrics + port: metrics + scheme: https + tlsConfig: + ca: + secret: + key: ca.crt + name: catalogd-service-cert-git-version + cert: + secret: + key: tls.crt + name: catalogd-service-cert-git-version + insecureSkipVerify: false + keySecret: + key: tls.key + name: catalogd-service-cert-git-version + serverName: catalogd-service.{{ .Values.namespaces.olmv1.name }}.svc + selector: + matchLabels: + app.kubernetes.io/name: catalogd diff --git a/helm/prometheus/templates/servicemonitor-kubelet.yml b/helm/prometheus/templates/servicemonitor-kubelet.yml new file mode 100644 index 0000000000..18d078a1f9 --- /dev/null +++ b/helm/prometheus/templates/servicemonitor-kubelet.yml @@ -0,0 +1,45 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + k8s-app: kubelet + name: kubelet + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + endpoints: + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + honorLabels: true + interval: 10s + metricRelabelings: + - action: keep + regex: (operator-controller|catalogd).*;manager + sourceLabels: + - pod + - container + path: /metrics + port: https-metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + honorLabels: true + interval: 10s + metricRelabelings: + - action: keep + regex: (operator-controller|catalogd).*;manager + sourceLabels: + - pod + - container + path: /metrics/cadvisor + port: https-metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + jobLabel: k8s-app + namespaceSelector: + matchNames: + - kube-system + selector: + matchLabels: + k8s-app: kubelet diff --git a/helm/prometheus/templates/servicemonitor-operator-controller-controller-manager-metrics-monitor.yml b/helm/prometheus/templates/servicemonitor-operator-controller-controller-manager-metrics-monitor.yml new file mode 100644 index 0000000000..b77a090b22 --- /dev/null +++ b/helm/prometheus/templates/servicemonitor-operator-controller-controller-manager-metrics-monitor.yml @@ -0,0 +1,33 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: operator-controller-controller-manager-metrics-monitor + namespace: {{ .Values.namespaces.olmv1.name }} +spec: + endpoints: + - authorization: + credentials: + key: token + name: prometheus-metrics-token + interval: 10s + path: /metrics + port: https + scheme: https + tlsConfig: + ca: + secret: + key: ca.crt + name: olmv1-cert + cert: + secret: + key: tls.crt + name: olmv1-cert + insecureSkipVerify: false + keySecret: + key: tls.key + name: olmv1-cert + serverName: operator-controller-service.{{ .Values.namespaces.olmv1.name }}.svc + selector: + matchLabels: + control-plane: operator-controller-controller-manager diff --git a/helm/prometheus/values.yaml b/helm/prometheus/values.yaml new file mode 100644 index 0000000000..d73579da8d --- /dev/null +++ b/helm/prometheus/values.yaml @@ -0,0 +1,19 @@ +# Default values for OLMv1. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# List of components to include +options: + operatorController: + enabled: true + catalogd: + enabled: true + +# The set of namespaces +namespaces: + olmv1: + name: olmv1-system + prometheus: + name: olmv1-system + certManager: + name: cert-manager diff --git a/helm/tilt.yaml b/helm/tilt.yaml new file mode 100644 index 0000000000..0fe3bec1f7 --- /dev/null +++ b/helm/tilt.yaml @@ -0,0 +1,24 @@ +# experimental values for OLMv1. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Tilt is an exception to the multi-values case, +# as the Tilt runner only accepts a single values file + +options: + certManager: + enabled: true + tilt: + enabled: true + featureSet: experimental + operatorController: + features: + enabled: + - PreflightPermissions + - HelmChartSupport + disabled: + - WebhookProviderOpenshiftServiceCA + catalogd: + features: + enabled: + - APIV1MetasHandler diff --git a/internal/catalogd/OWNERS b/internal/catalogd/OWNERS new file mode 100644 index 0000000000..84b5bd1cca --- /dev/null +++ b/internal/catalogd/OWNERS @@ -0,0 +1,2 @@ +approvers: + - catalogd-approvers diff --git a/internal/catalogd/controllers/core/clustercatalog_controller.go b/internal/catalogd/controllers/core/clustercatalog_controller.go index d0597d3eec..e968db7b97 100644 --- a/internal/catalogd/controllers/core/clustercatalog_controller.go +++ b/internal/catalogd/controllers/core/clustercatalog_controller.go @@ -24,7 +24,7 @@ import ( "sync" "time" - "github.com/containers/image/v5/docker/reference" + "go.podman.io/image/v5/docker/reference" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -76,10 +76,6 @@ type storedCatalogData struct { observedGeneration int64 } -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/finalizers,verbs=update - // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // @@ -93,7 +89,7 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque defer l.Info("reconcile ending") existingCatsrc := ocv1.ClusterCatalog{} - if err := r.Client.Get(ctx, req.NamespacedName, &existingCatsrc); err != nil { + if err := r.Get(ctx, req.NamespacedName, &existingCatsrc); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -132,7 +128,7 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque reconciledCatsrc.Finalizers = finalizers if updateFinalizers { - if err := r.Client.Update(ctx, reconciledCatsrc); err != nil { + if err := r.Update(ctx, reconciledCatsrc); err != nil { reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err)) } } @@ -196,6 +192,14 @@ func (r *ClusterCatalogReconciler) reconcile(ctx context.Context, catalog *ocv1. return ctrl.Result{}, nil } + if catalog.GetDeletionTimestamp() != nil { + // If we've gotten here, that means the cluster catalog is being deleted, we've handled all of + // _our_ finalizers (above), but the cluster catalog is still present in the cluster, likely + // because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan + // deletion finalizer). + return ctrl.Result{}, nil + } + // TODO: The below algorithm to get the current state based on an in-memory // storedCatalogs map is a hack that helps us keep the ClusterCatalog's // status up-to-date. The fact that we need this setup is indicative of diff --git a/internal/catalogd/controllers/core/clustercatalog_controller_test.go b/internal/catalogd/controllers/core/clustercatalog_controller_test.go index f7b917dc1e..76efafa228 100644 --- a/internal/catalogd/controllers/core/clustercatalog_controller_test.go +++ b/internal/catalogd/controllers/core/clustercatalog_controller_test.go @@ -10,11 +10,11 @@ import ( "testing/fstest" "time" - "github.com/containers/image/v5/docker/reference" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.podman.io/image/v5/docker/reference" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -766,6 +766,40 @@ func TestCatalogdControllerReconcile(t *testing.T) { }, }, }, + { + name: "reconcile should be short-circuited if the clustercatalog has a deletion timestamp and all known finalizers have been removed", + catalog: &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog", + Finalizers: []string{"finalizer"}, + DeletionTimestamp: &metav1.Time{Time: time.Date(2025, 6, 10, 16, 43, 0, 0, time.UTC)}, + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: "my.org/someimage:latest", + }, + }, + AvailabilityMode: ocv1.AvailabilityModeAvailable, + }, + }, + expectedCatalog: &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "catalog", + Finalizers: []string{"finalizer"}, + DeletionTimestamp: &metav1.Time{Time: time.Date(2025, 6, 10, 16, 43, 0, 0, time.UTC)}}, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: "my.org/someimage:latest", + }, + }, + AvailabilityMode: ocv1.AvailabilityModeAvailable, + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { reconciler := &ClusterCatalogReconciler{ diff --git a/internal/catalogd/controllers/core/pull_secret_controller.go b/internal/catalogd/controllers/core/pull_secret_controller.go deleted file mode 100644 index 8105810474..0000000000 --- a/internal/catalogd/controllers/core/pull_secret_controller.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package core - -import ( - "context" - "fmt" - "os" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -// PullSecretReconciler reconciles a specific Secret object -// that contains global pull secrets for pulling Catalog images -type PullSecretReconciler struct { - client.Client - SecretKey types.NamespacedName - AuthFilePath string -} - -func (r *PullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - if req.Name != r.SecretKey.Name || req.Namespace != r.SecretKey.Namespace { - logger.Error(fmt.Errorf("received unexpected request for Secret %v/%v", req.Namespace, req.Name), "reconciliation error") - return ctrl.Result{}, nil - } - - secret := &corev1.Secret{} - err := r.Get(ctx, req.NamespacedName, secret) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("secret not found") - return r.deleteSecretFile(logger) - } - logger.Error(err, "failed to get Secret") - return ctrl.Result{}, err - } - - return r.writeSecretToFile(logger, secret) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { - _, err := ctrl.NewControllerManagedBy(mgr). - For(&corev1.Secret{}). - Named("catalogd-pull-secret-controller"). - WithEventFilter(newSecretPredicate(r.SecretKey)). - Build(r) - - return err -} - -func newSecretPredicate(key types.NamespacedName) predicate.Predicate { - return predicate.NewPredicateFuncs(func(obj client.Object) bool { - return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace - }) -} - -// writeSecretToFile writes the secret data to the specified file -func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secret *corev1.Secret) (ctrl.Result, error) { - // image registry secrets are always stored with the key .dockerconfigjson - // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials - dockerConfigJSON, ok := secret.Data[".dockerconfigjson"] - if !ok { - logger.Error(fmt.Errorf("expected secret.Data key not found"), "expected secret Data to contain key .dockerconfigjson") - return ctrl.Result{}, nil - } - // expected format for auth.json - // https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md - err := os.WriteFile(r.AuthFilePath, dockerConfigJSON, 0600) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err) - } - logger.Info("saved global pull secret data locally") - return ctrl.Result{}, nil -} - -// deleteSecretFile deletes the auth file if the secret is deleted -func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) { - logger.Info("deleting local auth file", "file", r.AuthFilePath) - if err := os.Remove(r.AuthFilePath); err != nil { - if os.IsNotExist(err) { - logger.Info("auth file does not exist, nothing to delete") - return ctrl.Result{}, nil - } - return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err) - } - logger.Info("auth file deleted successfully") - return ctrl.Result{}, nil -} diff --git a/internal/catalogd/controllers/core/pull_secret_controller_test.go b/internal/catalogd/controllers/core/pull_secret_controller_test.go deleted file mode 100644 index 8b91da340f..0000000000 --- a/internal/catalogd/controllers/core/pull_secret_controller_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package core - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestSecretSyncerReconciler(t *testing.T) { - secretData := []byte(`{"auths":{"exampleRegistry": "exampledata"}}`) - authFileName := "test-auth.json" - for _, tt := range []struct { - name string - secret *corev1.Secret - addSecret bool - wantErr string - fileShouldExistBefore bool - fileShouldExistAfter bool - }{ - { - name: "secret exists, content gets saved to authFile", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: true, - fileShouldExistBefore: false, - fileShouldExistAfter: true, - }, - { - name: "secret does not exist, file exists previously, file should get deleted", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: false, - fileShouldExistBefore: true, - fileShouldExistAfter: false, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - tempAuthFile := filepath.Join(t.TempDir(), authFileName) - clientBuilder := fake.NewClientBuilder() - if tt.addSecret { - clientBuilder = clientBuilder.WithObjects(tt.secret) - } - cl := clientBuilder.Build() - - secretKey := types.NamespacedName{Namespace: tt.secret.Namespace, Name: tt.secret.Name} - r := &PullSecretReconciler{ - Client: cl, - SecretKey: secretKey, - AuthFilePath: tempAuthFile, - } - if tt.fileShouldExistBefore { - err := os.WriteFile(tempAuthFile, secretData, 0600) - require.NoError(t, err) - } - res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: secretKey}) - if tt.wantErr == "" { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, tt.wantErr) - } - require.Equal(t, ctrl.Result{}, res) - - if tt.fileShouldExistAfter { - _, err := os.Stat(tempAuthFile) - require.NoError(t, err) - } else { - _, err := os.Stat(tempAuthFile) - require.True(t, os.IsNotExist(err)) - } - }) - } -} diff --git a/internal/catalogd/storage/localdir_test.go b/internal/catalogd/storage/localdir_test.go index a6c861e773..72aafba1c7 100644 --- a/internal/catalogd/storage/localdir_test.go +++ b/internal/catalogd/storage/localdir_test.go @@ -401,7 +401,7 @@ func TestMetasEndpoint(t *testing.T) { require.Equal(t, tc.expectedStatusCode, resp.StatusCode) actualContent, err = io.ReadAll(resp.Body) require.NoError(t, err) - require.Equal(t, "", string(actualContent)) // HEAD should not return a body + require.Empty(t, string(actualContent)) // HEAD should not return a body resp.Body.Close() // And make sure any other method is not allowed diff --git a/internal/catalogd/webhook/cluster_catalog_webhook.go b/internal/catalogd/webhook/cluster_catalog_webhook.go index a19a62e732..3aea45d5d7 100644 --- a/internal/catalogd/webhook/cluster_catalog_webhook.go +++ b/internal/catalogd/webhook/cluster_catalog_webhook.go @@ -11,10 +11,6 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" ) -// +kubebuilder:webhook:admissionReviewVersions={v1},failurePolicy=Fail,groups=olm.operatorframework.io,mutating=true,name=inject-metadata-name.olm.operatorframework.io,path=/mutate-olm-operatorframework-io-v1-clustercatalog,resources=clustercatalogs,verbs=create;update,versions=v1,sideEffects=None,timeoutSeconds=10 - -// +kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=get;list;watch;patch;update - // ClusterCatalog wraps the external v1.ClusterCatalog type and implements admission.Defaulter type ClusterCatalog struct{} diff --git a/internal/operator-controller/OWNERS b/internal/operator-controller/OWNERS new file mode 100644 index 0000000000..3174715ba6 --- /dev/null +++ b/internal/operator-controller/OWNERS @@ -0,0 +1,2 @@ +approvers: + - operator-controller-approvers diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go new file mode 100644 index 0000000000..fa3f85e790 --- /dev/null +++ b/internal/operator-controller/applier/boxcutter.go @@ -0,0 +1,384 @@ +package applier + +import ( + "cmp" + "context" + "errors" + "fmt" + "io/fs" + "maps" + "slices" + "strings" + + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/yaml" + + helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/internal/operator-controller/labels" +) + +const ( + ClusterExtensionRevisionPreviousLimit = 5 +) + +type ClusterExtensionRevisionGenerator interface { + GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) + GenerateRevisionFromHelmRelease( + helmRelease *release.Release, ext *ocv1.ClusterExtension, + objectLabels map[string]string, + ) (*ocv1.ClusterExtensionRevision, error) +} + +type SimpleRevisionGenerator struct { + Scheme *runtime.Scheme + ManifestProvider ManifestProvider +} + +func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease( + helmRelease *release.Release, ext *ocv1.ClusterExtension, + objectLabels map[string]string, +) (*ocv1.ClusterExtensionRevision, error) { + docs := splitManifestDocuments(helmRelease.Manifest) + objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(docs)) + for _, doc := range docs { + obj := unstructured.Unstructured{} + if err := yaml.Unmarshal([]byte(doc), &obj); err != nil { + return nil, err + } + + labels := maps.Clone(obj.GetLabels()) + if labels == nil { + labels = map[string]string{} + } + maps.Copy(labels, objectLabels) + obj.SetLabels(labels) + obj.SetOwnerReferences(nil) // reset OwnerReferences for migration. + + objs = append(objs, ocv1.ClusterExtensionRevisionObject{ + Object: obj, + CollisionProtection: ocv1.CollisionProtectionNone, // allow to adopt objects from previous release + }) + } + + rev := r.buildClusterExtensionRevision(objs, ext, map[string]string{ + labels.BundleNameKey: helmRelease.Labels[labels.BundleNameKey], + labels.PackageNameKey: helmRelease.Labels[labels.PackageNameKey], + labels.BundleVersionKey: helmRelease.Labels[labels.BundleVersionKey], + labels.BundleReferenceKey: helmRelease.Labels[labels.BundleReferenceKey], + }) + rev.Name = fmt.Sprintf("%s-1", ext.Name) + rev.Spec.Revision = 1 + return rev, nil +} + +func (r *SimpleRevisionGenerator) GenerateRevision( + bundleFS fs.FS, ext *ocv1.ClusterExtension, + objectLabels, revisionAnnotations map[string]string, +) (*ocv1.ClusterExtensionRevision, error) { + // extract plain manifests + plain, err := r.ManifestProvider.Get(bundleFS, ext) + if err != nil { + return nil, err + } + + // objectLabels + objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain)) + for _, obj := range plain { + labels := maps.Clone(obj.GetLabels()) + if labels == nil { + labels = map[string]string{} + } + maps.Copy(labels, objectLabels) + obj.SetLabels(labels) + + gvk, err := apiutil.GVKForObject(obj, r.Scheme) + if err != nil { + return nil, err + } + + unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + unstr := unstructured.Unstructured{Object: unstrObj} + unstr.SetGroupVersionKind(gvk) + + objs = append(objs, ocv1.ClusterExtensionRevisionObject{ + Object: unstr, + }) + } + + if revisionAnnotations == nil { + revisionAnnotations = map[string]string{} + } + + return r.buildClusterExtensionRevision(objs, ext, revisionAnnotations), nil +} + +func (r *SimpleRevisionGenerator) buildClusterExtensionRevision( + objects []ocv1.ClusterExtensionRevisionObject, + ext *ocv1.ClusterExtension, + annotations map[string]string, +) *ocv1.ClusterExtensionRevision { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: annotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: PhaseSort(objects), + }, + } +} + +type BoxcutterStorageMigrator struct { + ActionClientGetter helmclient.ActionClientGetter + RevisionGenerator ClusterExtensionRevisionGenerator + Client boxcutterStorageMigratorClient +} + +type boxcutterStorageMigratorClient interface { + List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error + Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error +} + +func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error { + existingRevisionList := ocv1.ClusterExtensionRevisionList{} + if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }); err != nil { + return fmt.Errorf("listing ClusterExtensionRevisions before attempting migration: %w", err) + } + if len(existingRevisionList.Items) != 0 { + // No migration needed. + return nil + } + + ac, err := m.ActionClientGetter.ActionClientFor(ctx, ext) + if err != nil { + return err + } + + helmRelease, err := ac.Get(ext.GetName()) + if errors.Is(err, driver.ErrReleaseNotFound) { + // no Helm Release -> no prior installation. + return nil + } + if err != nil { + return err + } + + rev, err := m.RevisionGenerator.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels) + if err != nil { + return err + } + + if err := m.Client.Create(ctx, rev); err != nil { + return err + } + return nil +} + +type Boxcutter struct { + Client client.Client + Scheme *runtime.Scheme + RevisionGenerator ClusterExtensionRevisionGenerator + Preflights []Preflight + FieldOwner string +} + +func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) { + return bc.apply(ctx, contentFS, ext, objectLabels, revisionAnnotations) +} + +func (bc *Boxcutter) getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object { + var objs []client.Object + for _, phase := range rev.Spec.Phases { + for _, phaseObject := range phase.Objects { + objs = append(objs, &phaseObject.Object) + } + } + return objs +} + +func (bc *Boxcutter) createOrUpdate(ctx context.Context, obj client.Object) error { + if obj.GetObjectKind().GroupVersionKind().Empty() { + gvk, err := apiutil.GVKForObject(obj, bc.Scheme) + if err != nil { + return err + } + obj.GetObjectKind().SetGroupVersionKind(gvk) + } + return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership) +} + +func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) { + // Generate desired revision + desiredRevision, err := bc.RevisionGenerator.GenerateRevision(contentFS, ext, objectLabels, revisionAnnotations) + if err != nil { + return false, "", err + } + + if err := controllerutil.SetControllerReference(ext, desiredRevision, bc.Scheme); err != nil { + return false, "", fmt.Errorf("set ownerref: %w", err) + } + + // List all existing revisions + existingRevisions, err := bc.getExistingRevisions(ctx, ext.GetName()) + if err != nil { + return false, "", err + } + + currentRevision := &ocv1.ClusterExtensionRevision{} + state := StateNeedsInstall + // check if we can update the current revision. + if len(existingRevisions) > 0 { + // try first to update the current revision. + currentRevision = &existingRevisions[len(existingRevisions)-1] + desiredRevision.Spec.Previous = currentRevision.Spec.Previous + desiredRevision.Spec.Revision = currentRevision.Spec.Revision + desiredRevision.Name = currentRevision.Name + + err := bc.createOrUpdate(ctx, desiredRevision) + switch { + case apierrors.IsInvalid(err): + // We could not update the current revision due to trying to update an immutable field. + // Therefore, we need to create a new revision. + state = StateNeedsUpgrade + case err == nil: + // inplace patch was successful, no changes in phases + state = StateUnchanged + default: + return false, "", fmt.Errorf("patching %s Revision: %w", desiredRevision.Name, err) + } + } + + // Preflights + plainObjs := bc.getObjects(desiredRevision) + for _, preflight := range bc.Preflights { + if shouldSkipPreflight(ctx, preflight, ext, state) { + continue + } + switch state { + case StateNeedsInstall: + err := preflight.Install(ctx, plainObjs) + if err != nil { + return false, "", err + } + // TODO: jlanford's IDE says that "StateNeedsUpgrade" condition is always true, but + // it isn't immediately obvious why that is. Perhaps len(existingRevisions) is + // always greater than 0 (seems unlikely), or shouldSkipPreflight always returns + // true (and we continue) when state == StateNeedsInstall? + case StateNeedsUpgrade: + err := preflight.Upgrade(ctx, plainObjs) + if err != nil { + return false, "", err + } + } + } + + if state != StateUnchanged { + // need to create new revision + prevRevisions := existingRevisions + revisionNumber := latestRevisionNumber(prevRevisions) + 1 + + desiredRevision.Name = fmt.Sprintf("%s-%d", ext.Name, revisionNumber) + desiredRevision.Spec.Revision = revisionNumber + + if err = bc.setPreviousRevisions(ctx, desiredRevision, prevRevisions); err != nil { + return false, "", fmt.Errorf("garbage collecting old Revisions: %w", err) + } + + if err := bc.createOrUpdate(ctx, desiredRevision); err != nil { + return false, "", fmt.Errorf("creating new Revision: %w", err) + } + currentRevision = desiredRevision + } + + progressingCondition := meta.FindStatusCondition(currentRevision.Status.Conditions, ocv1.TypeProgressing) + availableCondition := meta.FindStatusCondition(currentRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + succeededCondition := meta.FindStatusCondition(currentRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + + if progressingCondition == nil && availableCondition == nil && succeededCondition == nil { + return false, "New revision created", nil + } else if progressingCondition != nil && progressingCondition.Status == metav1.ConditionTrue { + return false, progressingCondition.Message, nil + } else if availableCondition != nil && availableCondition.Status != metav1.ConditionTrue { + return false, "", errors.New(availableCondition.Message) + } else if succeededCondition != nil && succeededCondition.Status != metav1.ConditionTrue { + return false, succeededCondition.Message, nil + } + return true, "", nil +} + +// setPreviousRevisions populates spec.previous of latestRevision, trimming the list of previous _archived_ revisions down to +// ClusterExtensionRevisionPreviousLimit or to the first _active_ revision and deletes trimmed revisions from the cluster. +// NOTE: revisionList must be sorted in chronographical order, from oldest to latest. +func (bc *Boxcutter) setPreviousRevisions(ctx context.Context, latestRevision *ocv1.ClusterExtensionRevision, revisionList []ocv1.ClusterExtensionRevision) error { + trimmedPrevious := make([]ocv1.ClusterExtensionRevisionPrevious, 0) + for index, r := range revisionList { + if index < len(revisionList)-ClusterExtensionRevisionPreviousLimit && r.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { + // Delete oldest CREs from the cluster and list to reach ClusterExtensionRevisionPreviousLimit or latest active revision + if err := bc.Client.Delete(ctx, &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.Name, + }, + }); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("deleting previous archived Revision: %w", err) + } + } else { + // All revisions within the limit or still active are preserved + trimmedPrevious = append(trimmedPrevious, ocv1.ClusterExtensionRevisionPrevious{Name: r.Name, UID: r.GetUID()}) + } + } + latestRevision.Spec.Previous = trimmedPrevious + return nil +} + +// getExistingRevisions returns the list of ClusterExtensionRevisions for a ClusterExtension with name extName in revision order (oldest to newest) +func (bc *Boxcutter) getExistingRevisions(ctx context.Context, extName string) ([]ocv1.ClusterExtensionRevision, error) { + existingRevisionList := &ocv1.ClusterExtensionRevisionList{} + if err := bc.Client.List(ctx, existingRevisionList, client.MatchingLabels{ + controllers.ClusterExtensionRevisionOwnerLabel: extName, + }); err != nil { + return nil, fmt.Errorf("listing revisions: %w", err) + } + slices.SortFunc(existingRevisionList.Items, func(a, b ocv1.ClusterExtensionRevision) int { + return cmp.Compare(a.Spec.Revision, b.Spec.Revision) + }) + return existingRevisionList.Items, nil +} + +func latestRevisionNumber(prevRevisions []ocv1.ClusterExtensionRevision) int64 { + if len(prevRevisions) == 0 { + return 0 + } + return prevRevisions[len(prevRevisions)-1].Spec.Revision +} + +func splitManifestDocuments(file string) []string { + //nolint:prealloc + var docs []string + for _, manifest := range strings.Split(file, "\n") { + manifest = strings.TrimSpace(manifest) + if len(manifest) == 0 { + continue + } + docs = append(docs, manifest) + } + return docs +} diff --git a/internal/operator-controller/applier/boxcutter_test.go b/internal/operator-controller/applier/boxcutter_test.go new file mode 100644 index 0000000000..9da1ddb4a0 --- /dev/null +++ b/internal/operator-controller/applier/boxcutter_test.go @@ -0,0 +1,923 @@ +package applier_test + +import ( + "context" + "errors" + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + k8scheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/applier" + "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/internal/operator-controller/labels" +) + +func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T) { + g := &applier.SimpleRevisionGenerator{} + + helmRelease := &release.Release{ + Name: "test-123", + Manifest: `{"apiVersion":"v1","kind":"ConfigMap"}` + "\n" + `{"apiVersion":"v1","kind":"Secret"}` + "\n", + Labels: map[string]string{ + labels.BundleNameKey: "my-bundle", + labels.PackageNameKey: "my-package", + labels.BundleVersionKey: "1.2.0", + labels.BundleReferenceKey: "bundle-ref", + }, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-123", + }, + } + + objectLabels := map[string]string{ + "my-label": "my-value", + } + + rev, err := g.GenerateRevisionFromHelmRelease(helmRelease, ext, objectLabels) + require.NoError(t, err) + + assert.Equal(t, &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-123-1", + Annotations: map[string]string{ + "olm.operatorframework.io/bundle-name": "my-bundle", + "olm.operatorframework.io/bundle-reference": "bundle-ref", + "olm.operatorframework.io/bundle-version": "1.2.0", + "olm.operatorframework.io/package-name": "my-package", + }, + Labels: map[string]string{ + "olm.operatorframework.io/owner": "test-123", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: "deploy", + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "my-label": "my-value", + }, + }, + }, + }, + CollisionProtection: ocv1.CollisionProtectionNone, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "my-label": "my-value", + }, + }, + }, + }, + CollisionProtection: ocv1.CollisionProtectionNone, + }, + }, + }, + }, + }, + }, rev) +} + +func Test_SimpleRevisionGenerator_GenerateRevision(t *testing.T) { + r := &FakeManifestProvider{ + GetFn: func(_ fs.FS, _ *ocv1.ClusterExtension) ([]client.Object, error) { + return []client.Object{ + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + }, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-deployment", + }, + }, + }, nil + }, + } + + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-extension", + }, + } + + rev, err := b.GenerateRevision(fstest.MapFS{}, ext, map[string]string{}, map[string]string{}) + require.NoError(t, err) + + t.Log("by checking the olm.operatorframework.io/owner label is set to the name of the ClusterExtension") + require.Equal(t, map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: "test-extension", + }, rev.Labels) + t.Log("by checking there are no annotations") + require.Empty(t, rev.Annotations) + t.Log("by checking the revision number is 0") + require.Equal(t, int64(0), rev.Spec.Revision) + t.Log("by checking the rendered objects are present in the correct phases") + require.Equal(t, []ocv1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test-service", + }, + "spec": map[string]interface{}{}, + "status": map[string]interface{}{ + "loadBalancer": map[string]interface{}{}, + }, + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test-deployment", + }, + "spec": map[string]interface{}{ + "selector": nil, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{}, + "spec": map[string]interface{}{ + "containers": nil, + }, + }, + "strategy": map[string]interface{}{}, + }, + "status": map[string]interface{}{}, + }, + }, + }, + }, + }, + }, rev.Spec.Phases) +} + +func Test_SimpleRevisionGenerator_Renderer_Integration(t *testing.T) { + bundleFS := fstest.MapFS{} + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-extension", + }, + } + r := &FakeManifestProvider{ + GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) { + t.Log("by checking renderer was called with the correct parameters") + require.Equal(t, bundleFS, b) + require.Equal(t, ext, e) + return nil, nil + }, + } + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + _, err := b.GenerateRevision(bundleFS, ext, map[string]string{}, map[string]string{}) + require.NoError(t, err) +} + +func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *testing.T) { + renderedObjs := []client.Object{ + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Labels: map[string]string{ + "app": "test-obj", + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Labels: map[string]string{ + "app": "test-obj", + }, + }, + }, + } + r := &FakeManifestProvider{ + GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) { + return renderedObjs, nil + }, + } + + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + revAnnotations := map[string]string{ + "other": "value", + } + + rev, err := b.GenerateRevision(fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{ + "some": "value", + }, revAnnotations) + require.NoError(t, err) + t.Log("by checking the rendered objects contain the given object labels") + for _, phase := range rev.Spec.Phases { + for _, revObj := range phase.Objects { + require.Equal(t, map[string]string{ + "app": "test-obj", + "some": "value", + }, revObj.Object.GetLabels()) + } + } + t.Log("by checking the generated revision contain the given annotations") + require.Equal(t, revAnnotations, rev.Annotations) +} + +func Test_SimpleRevisionGenerator_Failure(t *testing.T) { + r := &FakeManifestProvider{ + GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) { + return nil, fmt.Errorf("some-error") + }, + } + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + rev, err := b.GenerateRevision(fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{}, map[string]string{}) + require.Nil(t, rev) + t.Log("by checking rendering errors are propagated") + require.Error(t, err) + require.Contains(t, err.Error(), "some-error") +} + +func TestBoxcutter_Apply(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + // This is the revision that the mock builder will produce by default. + // We calculate its hash to use in the tests. + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + UID: "test-uid", + }, + } + defaultDesiredRevision := &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext-1", + UID: "rev-uid-1", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-cm", + }, + }, + }, + }, + }, + }, + }, + }, + } + + allowedRevisionValue := func(revNum int64) *interceptor.Funcs { + return &interceptor.Funcs{ + Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + cer, ok := obj.(*ocv1.ClusterExtensionRevision) + if !ok { + return fmt.Errorf("expected ClusterExtensionRevision, got %T", obj) + } + fmt.Println(cer.Spec.Revision) + if cer.Spec.Revision != revNum { + fmt.Println("AAA") + return apierrors.NewInvalid(cer.GroupVersionKind().GroupKind(), cer.GetName(), field.ErrorList{field.Invalid(field.NewPath("spec.phases"), "immutable", "spec.phases is immutable")}) + } + return client.Patch(ctx, obj, patch, opts...) + }, + } + } + testCases := []struct { + name string + mockBuilder applier.ClusterExtensionRevisionGenerator + existingObjs []client.Object + expectedErr string + validate func(t *testing.T, c client.Client) + clientIterceptor *interceptor.Funcs + }{ + { + name: "first revision", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: revisionAnnotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-cm", + }, + }, + }, + }, + }, + }, + }, + }, + }, nil + }, + }, + validate: func(t *testing.T, c client.Client) { + revList := &ocv1.ClusterExtensionRevisionList{} + err := c.List(t.Context(), revList, client.MatchingLabels{controllers.ClusterExtensionRevisionOwnerLabel: ext.Name}) + require.NoError(t, err) + require.Len(t, revList.Items, 1) + + rev := revList.Items[0] + assert.Equal(t, "test-ext-1", rev.Name) + assert.Equal(t, int64(1), rev.Spec.Revision) + assert.Len(t, rev.OwnerReferences, 1) + assert.Equal(t, ext.Name, rev.OwnerReferences[0].Name) + assert.Equal(t, ext.UID, rev.OwnerReferences[0].UID) + }, + }, + { + name: "no change, revision exists", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: revisionAnnotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-cm", + }, + }, + }, + }, + }, + }, + }, + }, + }, nil + }, + }, + existingObjs: []client.Object{ + defaultDesiredRevision, + }, + validate: func(t *testing.T, c client.Client) { + revList := &ocv1.ClusterExtensionRevisionList{} + err := c.List(context.Background(), revList, client.MatchingLabels{controllers.ClusterExtensionRevisionOwnerLabel: ext.Name}) + require.NoError(t, err) + // No new revision should be created + require.Len(t, revList.Items, 1) + assert.Equal(t, "test-ext-1", revList.Items[0].Name) + }, + }, + { + name: "new revision created when objects in new revision are different", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: revisionAnnotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "new-secret", + }, + }, + }, + }, + }, + }, + }, + }, + }, nil + }, + }, + clientIterceptor: allowedRevisionValue(2), + existingObjs: []client.Object{ + defaultDesiredRevision, + }, + validate: func(t *testing.T, c client.Client) { + revList := &ocv1.ClusterExtensionRevisionList{} + err := c.List(context.Background(), revList, client.MatchingLabels{controllers.ClusterExtensionRevisionOwnerLabel: ext.Name}) + require.NoError(t, err) + require.Len(t, revList.Items, 2) + + // Find the new revision (rev 2) + var newRev ocv1.ClusterExtensionRevision + for _, r := range revList.Items { + if r.Spec.Revision == 2 { + newRev = r + break + } + } + require.NotNil(t, newRev) + + assert.Equal(t, "test-ext-2", newRev.Name) + assert.Equal(t, int64(2), newRev.Spec.Revision) + require.Len(t, newRev.Spec.Previous, 1) + assert.Equal(t, "test-ext-1", newRev.Spec.Previous[0].Name) + assert.Equal(t, types.UID("rev-uid-1"), newRev.Spec.Previous[0].UID) + }, + }, + { + name: "error from GenerateRevision", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return nil, errors.New("render boom") + }, + }, + expectedErr: "render boom", + validate: func(t *testing.T, c client.Client) { + // Ensure no revisions were created + revList := &ocv1.ClusterExtensionRevisionList{} + err := c.List(context.Background(), revList, client.MatchingLabels{controllers.ClusterExtensionRevisionOwnerLabel: ext.Name}) + require.NoError(t, err) + assert.Empty(t, revList.Items) + }, + }, + { + name: "keep at most 5 past revisions", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: revisionAnnotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{}, + }, nil + }, + }, + existingObjs: []client.Object{ + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-1", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 1, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-2", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 2, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-3", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 3, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-4", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 4, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-5", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 5, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-6", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 6, + }, + }, + }, + clientIterceptor: allowedRevisionValue(7), + validate: func(t *testing.T, c client.Client) { + rev1 := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{Name: "rev-1"}, rev1) + require.Error(t, err) + assert.True(t, apierrors.IsNotFound(err)) + + latest := &ocv1.ClusterExtensionRevision{} + err = c.Get(t.Context(), client.ObjectKey{Name: "test-ext-7"}, latest) + require.NoError(t, err) + assert.Len(t, latest.Spec.Previous, applier.ClusterExtensionRevisionPreviousLimit) + }, + }, + { + name: "keep active revisions when they are out of limit", + mockBuilder: &mockBundleRevisionBuilder{ + makeRevisionFunc: func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: revisionAnnotations, + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{}, + }, nil + }, + }, + existingObjs: []client.Object{ + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-1", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 1, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-2", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + // index beyond the retention limit but active; should be preserved + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 2, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-3", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 3, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-4", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + // archived but should be preserved since it is within the limit + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateArchived, + Revision: 4, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-5", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 5, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-6", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 6, + }, + }, + &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev-7", + Labels: map[string]string{ + controllers.ClusterExtensionRevisionOwnerLabel: ext.Name, + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 7, + }, + }, + }, + clientIterceptor: allowedRevisionValue(8), + validate: func(t *testing.T, c client.Client) { + rev1 := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{Name: "rev-1"}, rev1) + require.Error(t, err) + assert.True(t, apierrors.IsNotFound(err)) + + rev2 := &ocv1.ClusterExtensionRevision{} + err = c.Get(t.Context(), client.ObjectKey{Name: "rev-2"}, rev2) + require.NoError(t, err) + + rev4 := &ocv1.ClusterExtensionRevision{} + err = c.Get(t.Context(), client.ObjectKey{Name: "rev-4"}, rev4) + require.NoError(t, err) + + latest := &ocv1.ClusterExtensionRevision{} + err = c.Get(t.Context(), client.ObjectKey{Name: "test-ext-8"}, latest) + require.NoError(t, err) + assert.Len(t, latest.Spec.Previous, 6) + assert.Contains(t, latest.Spec.Previous, ocv1.ClusterExtensionRevisionPrevious{Name: "rev-4"}) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup + cb := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(tc.existingObjs...) + if tc.clientIterceptor != nil { + cb.WithInterceptorFuncs(*tc.clientIterceptor) + } + fakeClient := cb.Build() + + boxcutter := &applier.Boxcutter{ + Client: fakeClient, + Scheme: testScheme, + RevisionGenerator: tc.mockBuilder, + FieldOwner: "test-owner", + } + + // We need a dummy fs.FS + testFS := fstest.MapFS{} + + // Execute + installSucceeded, installStatus, err := boxcutter.Apply(t.Context(), testFS, ext, nil, nil) + + // Assert + if tc.expectedErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + assert.False(t, installSucceeded) + assert.Empty(t, installStatus) + } else { + require.NoError(t, err) + assert.False(t, installSucceeded) + assert.Equal(t, "New revision created", installStatus) + } + + if tc.validate != nil { + // For the client create error, we need a client that *will* error. + // Since we can't do that easily, we will skip validation for that specific path + // as the state won't be what we expect. + if tc.name != "error from client create" { + tc.validate(t, fakeClient) + } + } + }) + } +} + +func TestBoxcutterStorageMigrator(t *testing.T) { + t.Run("creates revision", func(t *testing.T) { + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{} + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + client. + On("List", mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevisionList"), mock.Anything). + Return(nil) + client. + On("Create", mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). + Once(). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + }) + + t.Run("does not create revision when revisions exist", func(t *testing.T) { + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{} + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + client. + On("List", mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevisionList"), mock.Anything). + Run(func(args mock.Arguments) { + list := args.Get(1).(*ocv1.ClusterExtensionRevisionList) + list.Items = []ocv1.ClusterExtensionRevision{ + {}, {}, // Existing revisions. + } + }). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + }) + + t.Run("does not create revision when no helm release", func(t *testing.T) { + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{ + getClientErr: driver.ErrReleaseNotFound, + } + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + client. + On("List", mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevisionList"), mock.Anything). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + }) +} + +// mockBundleRevisionBuilder is a mock implementation of the ClusterExtensionRevisionGenerator for testing. +type mockBundleRevisionBuilder struct { + makeRevisionFunc func(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotation map[string]string) (*ocv1.ClusterExtensionRevision, error) +} + +func (m *mockBundleRevisionBuilder) GenerateRevision(bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return m.makeRevisionFunc(bundleFS, ext, objectLabels, revisionAnnotations) +} + +func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease( + helmRelease *release.Release, ext *ocv1.ClusterExtension, + objectLabels map[string]string, +) (*ocv1.ClusterExtensionRevision, error) { + return nil, nil +} + +type clientMock struct { + mock.Mock +} + +func (m *clientMock) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + args := m.Called(ctx, list, opts) + return args.Error(0) +} + +func (m *clientMock) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + args := m.Called(ctx, obj, opts) + return args.Error(0) +} diff --git a/internal/operator-controller/applier/helm.go b/internal/operator-controller/applier/helm.go index cc47cc5a36..4e70268941 100644 --- a/internal/operator-controller/applier/helm.go +++ b/internal/operator-controller/applier/helm.go @@ -19,70 +19,53 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apimachyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" + "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" + imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" ) -const ( - StateNeedsInstall string = "NeedsInstall" - StateNeedsUpgrade string = "NeedsUpgrade" - StateUnchanged string = "Unchanged" - StateError string = "Error" - maxHelmReleaseHistory = 10 -) - -// Preflight is a check that should be run before making any changes to the cluster -type Preflight interface { - // Install runs checks that should be successful prior - // to installing the Helm release. It is provided - // a Helm release and returns an error if the - // check is unsuccessful - Install(context.Context, *release.Release) error - - // Upgrade runs checks that should be successful prior - // to upgrading the Helm release. It is provided - // a Helm release and returns an error if the - // check is unsuccessful - Upgrade(context.Context, *release.Release) error +// HelmChartProvider provides helm charts from bundle sources and cluster extensions +type HelmChartProvider interface { + Get(bundle fs.FS, clusterExtension *ocv1.ClusterExtension) (*chart.Chart, error) } -type BundleToHelmChartConverter interface { - ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) +type HelmReleaseToObjectsConverter struct { } -type Helm struct { - ActionClientGetter helmclient.ActionClientGetter - Preflights []Preflight - PreAuthorizer authorization.PreAuthorizer - BundleToHelmChartConverter BundleToHelmChartConverter +type HelmReleaseToObjectsConverterInterface interface { + GetObjectsFromRelease(rel *release.Release) ([]client.Object, error) } -// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND -// if it is set to enforcement None. -func shouldSkipPreflight(ctx context.Context, preflight Preflight, ext *ocv1.ClusterExtension, state string) bool { - l := log.FromContext(ctx) - hasCRDUpgradeSafety := ext.Spec.Install != nil && ext.Spec.Install.Preflight != nil && ext.Spec.Install.Preflight.CRDUpgradeSafety != nil - _, isCRDUpgradeSafetyInstance := preflight.(*crdupgradesafety.Preflight) +func (h HelmReleaseToObjectsConverter) GetObjectsFromRelease(rel *release.Release) ([]client.Object, error) { + if rel == nil { + return nil, nil + } - if hasCRDUpgradeSafety && isCRDUpgradeSafetyInstance { - if state == StateNeedsInstall || state == StateNeedsUpgrade { - l.Info("crdUpgradeSafety ", "policy", ext.Spec.Install.Preflight.CRDUpgradeSafety.Enforcement) - } - if ext.Spec.Install.Preflight.CRDUpgradeSafety.Enforcement == ocv1.CRDUpgradeSafetyEnforcementNone { - // Skip this preflight check because it is of type *crdupgradesafety.Preflight and the CRD Upgrade Safety - // policy is set to None - return true - } + relObjects, err := util.ManifestObjects(strings.NewReader(rel.Manifest), fmt.Sprintf("%s-release-manifest", rel.Name)) + if err != nil { + return nil, fmt.Errorf("parsing release %q objects: %w", rel.Name, err) } - return false + return relObjects, nil +} + +type Helm struct { + ActionClientGetter helmclient.ActionClientGetter + Preflights []Preflight + PreAuthorizer authorization.PreAuthorizer + HelmChartProvider HelmChartProvider + HelmReleaseToObjectsConverter HelmReleaseToObjectsConverterInterface + + Manager contentmanager.Manager + Watcher crcontroller.Controller } // runPreAuthorizationChecks performs pre-authorization checks for a Helm release @@ -91,7 +74,7 @@ func shouldSkipPreflight(ctx context.Context, preflight Preflight, ext *ocv1.Clu func (h *Helm) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterExtension, chart *chart.Chart, values chartutil.Values, post postrender.PostRenderer) error { tmplRel, err := h.renderClientOnlyRelease(ctx, ext, chart, values, post) if err != nil { - return fmt.Errorf("failed to get release state using client-only dry-run: %w", err) + return fmt.Errorf("error rendering content for pre-authorization checks: %w", err) } missingRules, authErr := h.PreAuthorizer.PreAuthorize(ctx, ext, strings.NewReader(tmplRel.Manifest)) @@ -119,10 +102,10 @@ func (h *Helm) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterE return nil } -func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) { +func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) (bool, string, error) { chrt, err := h.buildHelmChart(contentFS, ext) if err != nil { - return nil, "", err + return false, "", err } values := chartutil.Values{} @@ -134,18 +117,22 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte err := h.runPreAuthorizationChecks(ctx, ext, chrt, values, post) if err != nil { // Return the pre-authorization error directly - return nil, "", err + return false, "", err } } ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) if err != nil { - return nil, "", err + return false, "", err } rel, desiredRel, state, err := h.getReleaseState(ac, ext, chrt, values, post) if err != nil { - return nil, "", fmt.Errorf("failed to get release state using server-side dry-run: %w", err) + return false, "", fmt.Errorf("failed to get release state using server-side dry-run: %w", err) + } + objs, err := h.HelmReleaseToObjectsConverter.GetObjectsFromRelease(desiredRel) + if err != nil { + return false, "", err } for _, preflight := range h.Preflights { @@ -154,14 +141,14 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte } switch state { case StateNeedsInstall: - err := preflight.Install(ctx, desiredRel) + err := preflight.Install(ctx, objs) if err != nil { - return nil, state, err + return false, "", err } case StateNeedsUpgrade: - err := preflight.Upgrade(ctx, desiredRel) + err := preflight.Upgrade(ctx, objs) if err != nil { - return nil, state, err + return false, "", err } } } @@ -174,7 +161,7 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte return nil }, helmclient.AppendInstallPostRenderer(post)) if err != nil { - return nil, state, err + return false, "", err } case StateNeedsUpgrade: rel, err = ac.Upgrade(ext.GetName(), ext.Spec.Namespace, chrt, values, func(upgrade *action.Upgrade) error { @@ -183,33 +170,48 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte return nil }, helmclient.AppendUpgradePostRenderer(post)) if err != nil { - return nil, state, err + return false, "", err } case StateUnchanged: if err := ac.Reconcile(rel); err != nil { - return nil, state, err + return false, "", err } default: - return nil, state, fmt.Errorf("unexpected release state %q", state) + return false, "", fmt.Errorf("unexpected release state %q", state) } relObjects, err := util.ManifestObjects(strings.NewReader(rel.Manifest), fmt.Sprintf("%s-release-manifest", rel.Name)) if err != nil { - return nil, state, err + return true, "", err + } + klog.FromContext(ctx).Info("watching managed objects") + cache, err := h.Manager.Get(ctx, ext) + if err != nil { + return true, "", err } - return relObjects, state, nil + if err := cache.Watch(ctx, h.Watcher, relObjects...); err != nil { + return true, "", err + } + + return true, "", nil } func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { - if h.BundleToHelmChartConverter == nil { - return nil, errors.New("BundleToHelmChartConverter is nil") + if h.HelmChartProvider == nil { + return nil, errors.New("HelmChartProvider is nil") } - watchNamespace, err := GetWatchNamespace(ext) - if err != nil { - return nil, err + if features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) { + meta := new(chart.Metadata) + if ok, _ := imageutil.IsBundleSourceChart(bundleFS, meta); ok { + return imageutil.LoadChartFSWithOptions( + bundleFS, + fmt.Sprintf("%s-%s.tgz", meta.Name, meta.Version), + imageutil.WithInstallNamespace(ext.Spec.Namespace), + ) + } } - return h.BundleToHelmChartConverter.ToHelmChart(source.FromFS(bundleFS), ext.Spec.Namespace, watchNamespace) + return h.HelmChartProvider.Get(bundleFS, ext) } func (h *Helm) renderClientOnlyRelease(ctx context.Context, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, error) { diff --git a/internal/operator-controller/applier/helm_test.go b/internal/operator-controller/applier/helm_test.go index 66017eafad..8d07de9123 100644 --- a/internal/operator-controller/applier/helm_test.go +++ b/internal/operator-controller/applier/helm_test.go @@ -4,20 +4,17 @@ import ( "context" "errors" "io" + "io/fs" "os" "testing" "testing/fstest" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" - corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - featuregatetesting "k8s.io/component-base/featuregate/testing" "sigs.k8s.io/controller-runtime/pkg/client" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" @@ -25,11 +22,48 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/authorization" - "github.com/operator-framework/operator-controller/internal/operator-controller/features" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" + cmcache "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager/cache" ) +var _ contentmanager.Manager = (*mockManagedContentCacheManager)(nil) + +type mockManagedContentCacheManager struct { + err error + cache cmcache.Cache +} + +func (m *mockManagedContentCacheManager) Get(_ context.Context, _ *ocv1.ClusterExtension) (cmcache.Cache, error) { + if m.err != nil { + return nil, m.err + } + return m.cache, nil +} + +func (m *mockManagedContentCacheManager) Delete(_ *ocv1.ClusterExtension) error { + return m.err +} + +type mockManagedContentCache struct { + err error +} + +var _ cmcache.Cache = (*mockManagedContentCache)(nil) + +func (m *mockManagedContentCache) Close() error { + if m.err != nil { + return m.err + } + return nil +} + +func (m *mockManagedContentCache) Watch(_ context.Context, _ cmcache.Watcher, _ ...client.Object) error { + if m.err != nil { + return m.err + } + return nil +} + type mockPreflight struct { installErr error upgradeErr error @@ -48,14 +82,21 @@ func (p *mockPreAuthorizer) PreAuthorize( return p.missingRules, p.returnError } -func (mp *mockPreflight) Install(context.Context, *release.Release) error { +func (mp *mockPreflight) Install(context.Context, []client.Object) error { return mp.installErr } -func (mp *mockPreflight) Upgrade(context.Context, *release.Release) error { +func (mp *mockPreflight) Upgrade(context.Context, []client.Object) error { return mp.upgradeErr } +type mockHelmReleaseToObjectsConverter struct { +} + +func (mockHelmReleaseToObjectsConverter) GetObjectsFromRelease(*release.Release) ([]client.Object, error) { + return nil, nil +} + type mockActionGetter struct { actionClientForErr error getClientErr error @@ -188,38 +229,38 @@ func TestApply_Base(t *testing.T) { t.Run("fails converting content FS to helm chart", func(t *testing.T) { helmApplier := applier.Helm{} - objs, state, err := helmApplier.Apply(context.TODO(), os.DirFS("/"), testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), os.DirFS("/"), testCE, testObjectLabels, testStorageLabels) require.Error(t, err) - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails trying to obtain an action client", func(t *testing.T) { mockAcg := &mockActionGetter{actionClientForErr: errors.New("failed getting action client")} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "getting action client") - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails getting current release and !driver.ErrReleaseNotFound", func(t *testing.T) { mockAcg := &mockActionGetter{getClientErr: errors.New("failed getting current release")} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "getting current release") - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) } @@ -230,15 +271,15 @@ func TestApply_Installation(t *testing.T) { dryRunInstallErr: errors.New("failed attempting to dry-run install chart"), } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "attempting to dry-run install chart") - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during pre-flight installation", func(t *testing.T) { @@ -248,16 +289,17 @@ func TestApply_Installation(t *testing.T) { } mockPf := &mockPreflight{installErr: errors.New("failed during install pre-flight check")} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - Preflights: []applier.Preflight{mockPf}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "install pre-flight check") - require.Equal(t, applier.StateNeedsInstall, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during installation", func(t *testing.T) { @@ -266,15 +308,16 @@ func TestApply_Installation(t *testing.T) { installErr: errors.New("failed installing chart"), } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "installing chart") - require.Equal(t, applier.StateNeedsInstall, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("successful installation", func(t *testing.T) { @@ -286,16 +329,18 @@ func TestApply_Installation(t *testing.T) { }, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, + Manager: &mockManagedContentCacheManager{ + cache: &mockManagedContentCache{}, + }, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.NoError(t, err) - require.Equal(t, applier.StateNeedsInstall, state) - require.NotNil(t, objs) - assert.Equal(t, "service-a", objs[0].GetName()) - assert.Equal(t, "service-b", objs[1].GetName()) + require.Empty(t, installStatus) + require.True(t, installSucceeded) }) } @@ -306,15 +351,15 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { dryRunInstallErr: errors.New("failed attempting to dry-run install chart"), } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "attempting to dry-run install chart") - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during pre-flight installation", func(t *testing.T) { @@ -328,17 +373,18 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { } mockPf := &mockPreflight{installErr: errors.New("failed during install pre-flight check")} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - Preflights: []applier.Preflight{mockPf}, - PreAuthorizer: &mockPreAuthorizer{nil, nil}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + PreAuthorizer: &mockPreAuthorizer{nil, nil}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "install pre-flight check") - require.Equal(t, applier.StateNeedsInstall, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during installation because of pre-authorization failure", func(t *testing.T) { @@ -350,9 +396,9 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{nil, errPreAuth}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + PreAuthorizer: &mockPreAuthorizer{nil, errPreAuth}, + HelmChartProvider: DummyHelmChartProvider, } // Use a ClusterExtension with valid Spec fields. validCE := &ocv1.ClusterExtension{ @@ -363,11 +409,11 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, }, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "problem running preauthorization") - require.Equal(t, "", state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during installation due to missing RBAC rules", func(t *testing.T) { @@ -379,9 +425,9 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{missingRBAC, nil}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + PreAuthorizer: &mockPreAuthorizer{missingRBAC, nil}, + HelmChartProvider: DummyHelmChartProvider, } // Use a ClusterExtension with valid Spec fields. validCE := &ocv1.ClusterExtension{ @@ -392,11 +438,11 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, }, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, errMissingRBAC) - require.Equal(t, "", state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("successful installation", func(t *testing.T) { @@ -408,9 +454,13 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{nil, nil}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + PreAuthorizer: &mockPreAuthorizer{nil, nil}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, + Manager: &mockManagedContentCacheManager{ + cache: &mockManagedContentCache{}, + }, } // Use a ClusterExtension with valid Spec fields. @@ -423,12 +473,10 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, validCE, testObjectLabels, testStorageLabels) require.NoError(t, err) - require.Equal(t, applier.StateNeedsInstall, state) - require.NotNil(t, objs) - assert.Equal(t, "service-a", objs[0].GetName()) - assert.Equal(t, "service-b", objs[1].GetName()) + require.Empty(t, installStatus) + require.True(t, installSucceeded) }) } @@ -442,15 +490,15 @@ func TestApply_Upgrade(t *testing.T) { dryRunUpgradeErr: errors.New("failed attempting to dry-run upgrade chart"), } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "attempting to dry-run upgrade chart") - require.Nil(t, objs) - require.Empty(t, state) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during pre-flight upgrade", func(t *testing.T) { @@ -464,16 +512,17 @@ func TestApply_Upgrade(t *testing.T) { } mockPf := &mockPreflight{upgradeErr: errors.New("failed during upgrade pre-flight check")} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - Preflights: []applier.Preflight{mockPf}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "upgrade pre-flight check") - require.Equal(t, applier.StateNeedsUpgrade, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during upgrade", func(t *testing.T) { @@ -488,14 +537,15 @@ func TestApply_Upgrade(t *testing.T) { mockPf := &mockPreflight{} helmApplier := applier.Helm{ ActionClientGetter: mockAcg, Preflights: []applier.Preflight{mockPf}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "upgrading chart") - require.Equal(t, applier.StateNeedsUpgrade, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("fails during upgrade reconcile (StateUnchanged)", func(t *testing.T) { @@ -509,16 +559,17 @@ func TestApply_Upgrade(t *testing.T) { } mockPf := &mockPreflight{} helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - Preflights: []applier.Preflight{mockPf}, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) require.Error(t, err) require.ErrorContains(t, err, "reconciling charts") - require.Equal(t, applier.StateUnchanged, state) - require.Nil(t, objs) + require.False(t, installSucceeded) + require.Empty(t, installStatus) }) t.Run("successful upgrade", func(t *testing.T) { @@ -530,57 +581,23 @@ func TestApply_Upgrade(t *testing.T) { desiredRel: &testDesiredRelease, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - BundleToHelmChartConverter: &convert.BundleToHelmChartConverter{}, - } - - objs, state, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.NoError(t, err) - require.Equal(t, applier.StateNeedsUpgrade, state) - require.NotNil(t, objs) - assert.Equal(t, "service-a", objs[0].GetName()) - assert.Equal(t, "service-b", objs[1].GetName()) - }) -} - -func TestApply_InstallationWithSingleOwnNamespaceInstallSupportEnabled(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.SingleOwnNamespaceInstallSupport, true) - t.Run("generates bundle resources using the configured watch namespace", func(t *testing.T) { - var expectedWatchNamespace = "watch-namespace" - - helmApplier := applier.Helm{ - ActionClientGetter: &mockActionGetter{ - getClientErr: driver.ErrReleaseNotFound, - desiredRel: &release.Release{ - Info: &release.Info{Status: release.StatusDeployed}, - Manifest: validManifest, - }, - }, - BundleToHelmChartConverter: &fakeBundleToHelmChartConverter{ - fn: func(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) { - require.Equal(t, expectedWatchNamespace, watchNamespace) - return nil, nil - }, + ActionClientGetter: mockAcg, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, + Manager: &mockManagedContentCacheManager{ + cache: &mockManagedContentCache{}, }, } - testExt := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testExt", - Annotations: map[string]string{ - applier.AnnotationClusterExtensionWatchNamespace: expectedWatchNamespace, - }, - }, - } - - _, _, _ = helmApplier.Apply(context.TODO(), validFS, testExt, testObjectLabels, testStorageLabels) + installSucceeded, installStatus, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + require.NoError(t, err) + require.True(t, installSucceeded) + require.Empty(t, installStatus) }) } func TestApply_RegistryV1ToChartConverterIntegration(t *testing.T) { t.Run("generates bundle resources in AllNamespaces install mode", func(t *testing.T) { - var expectedWatchNamespace = corev1.NamespaceAll - helmApplier := applier.Helm{ ActionClientGetter: &mockActionGetter{ getClientErr: driver.ErrReleaseNotFound, @@ -589,12 +606,16 @@ func TestApply_RegistryV1ToChartConverterIntegration(t *testing.T) { Manifest: validManifest, }, }, - BundleToHelmChartConverter: &fakeBundleToHelmChartConverter{ - fn: func(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) { - require.Equal(t, expectedWatchNamespace, watchNamespace) + HelmChartProvider: &FakeHelmChartProvider{ + fn: func(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { + require.Equal(t, testCE, ext) return nil, nil }, }, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, + Manager: &mockManagedContentCacheManager{ + cache: &mockManagedContentCache{}, + }, } _, _, _ = helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) @@ -609,22 +630,31 @@ func TestApply_RegistryV1ToChartConverterIntegration(t *testing.T) { Manifest: validManifest, }, }, - BundleToHelmChartConverter: &fakeBundleToHelmChartConverter{ - fn: func(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) { + HelmChartProvider: &FakeHelmChartProvider{ + fn: func(bundleFs fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { return nil, errors.New("some error") }, }, + Manager: &mockManagedContentCacheManager{ + cache: &mockManagedContentCache{}, + }, } _, _, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) - require.Error(t, err) + require.ErrorContains(t, err, "some error") }) } -type fakeBundleToHelmChartConverter struct { - fn func(source.BundleSource, string, string) (*chart.Chart, error) +type FakeHelmChartProvider struct { + fn func(fs.FS, *ocv1.ClusterExtension) (*chart.Chart, error) +} + +func (f FakeHelmChartProvider) Get(bundle fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { + return f.fn(bundle, ext) } -func (f fakeBundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) { - return f.fn(bundle, installNamespace, watchNamespace) +var DummyHelmChartProvider = &FakeHelmChartProvider{ + fn: func(fs fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { + return &chart.Chart{}, nil + }, } diff --git a/internal/operator-controller/applier/phase.go b/internal/operator-controller/applier/phase.go new file mode 100644 index 0000000000..9ae31db6a7 --- /dev/null +++ b/internal/operator-controller/applier/phase.go @@ -0,0 +1,136 @@ +package applier + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +// The following, with modifications, is taken from: +// https://github.com/package-operator/package-operator/blob/v1.18.2/internal/packages/internal/packagekickstart/presets/phases.go +// +// Determines a phase using the objects Group Kind from a list of presets. +// Defaults to the `deploy` phase if no preset was found. Runtimes that +// depend on a custom resource to start i.e. certmanager's Certificate +// will require this. +func determinePhase(gk schema.GroupKind) Phase { + phase, ok := gkPhaseMap[gk] + if !ok { + return PhaseDeploy + } + return phase +} + +// Phase represents a well-known phase. +type Phase string + +const ( + PhaseNamespaces Phase = "namespaces" + PhasePolicies Phase = "policies" + PhaseRBAC Phase = "rbac" + PhaseCRDs Phase = "crds" + PhaseStorage Phase = "storage" + PhaseDeploy Phase = "deploy" + PhasePublish Phase = "publish" +) + +// Well known phases ordered. +var defaultPhaseOrder = []Phase{ + PhaseNamespaces, + PhasePolicies, + PhaseRBAC, + PhaseCRDs, + PhaseStorage, + PhaseDeploy, + PhasePublish, +} + +var ( + // This will be populated from `phaseGKMap` in an init func! + gkPhaseMap = map[schema.GroupKind]Phase{} + phaseGKMap = map[Phase][]schema.GroupKind{ + PhaseNamespaces: { + {Kind: "Namespace"}, + }, + + PhasePolicies: { + {Kind: "ResourceQuota"}, + {Kind: "LimitRange"}, + {Kind: "PriorityClass", Group: "scheduling.k8s.io"}, + {Kind: "NetworkPolicy", Group: "networking.k8s.io"}, + {Kind: "HorizontalPodAutoscaler", Group: "autoscaling"}, + {Kind: "PodDisruptionBudget", Group: "policy"}, + }, + + PhaseRBAC: { + {Kind: "ServiceAccount"}, + {Kind: "Role", Group: "rbac.authorization.k8s.io"}, + {Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"}, + {Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"}, + {Kind: "ClusterRoleBinding", Group: "rbac.authorization.k8s.io"}, + }, + + PhaseCRDs: { + {Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io"}, + }, + + PhaseStorage: { + {Kind: "PersistentVolume"}, + {Kind: "PersistentVolumeClaim"}, + {Kind: "StorageClass", Group: "storage.k8s.io"}, + }, + + PhaseDeploy: { + {Kind: "Deployment", Group: "apps"}, + {Kind: "DaemonSet", Group: "apps"}, + {Kind: "StatefulSet", Group: "apps"}, + {Kind: "ReplicaSet"}, + {Kind: "Pod"}, // probing complicated, may be either Completed or Available. + {Kind: "Job", Group: "batch"}, + {Kind: "CronJob", Group: "batch"}, + {Kind: "Service"}, + {Kind: "Secret"}, + {Kind: "ConfigMap"}, + }, + + PhasePublish: { + {Kind: "Ingress", Group: "networking.k8s.io"}, + {Kind: "APIService", Group: "apiregistration.k8s.io"}, + {Kind: "Route", Group: "route.openshift.io"}, + {Kind: "MutatingWebhookConfiguration", Group: "admissionregistration.k8s.io"}, + {Kind: "ValidatingWebhookConfiguration", Group: "admissionregistration.k8s.io"}, + }, + } +) + +func init() { + for phase, gks := range phaseGKMap { + for _, gk := range gks { + gkPhaseMap[gk] = phase + } + } +} + +// PhaseSort takes an unsorted list of objects and organizes them into sorted phases. +// Each phase will be applied in order according to DefaultPhaseOrder. Objects +// within a single phase are applied simultaneously. +func PhaseSort(unsortedObjs []ocv1.ClusterExtensionRevisionObject) []ocv1.ClusterExtensionRevisionPhase { + phasesSorted := make([]ocv1.ClusterExtensionRevisionPhase, 0) + phaseMap := make(map[Phase][]ocv1.ClusterExtensionRevisionObject, 0) + + for _, obj := range unsortedObjs { + phase := determinePhase(obj.Object.GroupVersionKind().GroupKind()) + phaseMap[phase] = append(phaseMap[phase], obj) + } + + for _, phaseName := range defaultPhaseOrder { + if objs, ok := phaseMap[phaseName]; ok { + phasesSorted = append(phasesSorted, ocv1.ClusterExtensionRevisionPhase{ + Name: string(phaseName), + Objects: objs, + }) + } + } + + return phasesSorted +} diff --git a/internal/operator-controller/applier/phase_test.go b/internal/operator-controller/applier/phase_test.go new file mode 100644 index 0000000000..3f2d85d0b1 --- /dev/null +++ b/internal/operator-controller/applier/phase_test.go @@ -0,0 +1,292 @@ +package applier_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + v1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/applier" +) + +func Test_PhaseSort(t *testing.T) { + for _, tt := range []struct { + name string + objs []v1.ClusterExtensionRevisionObject + want []v1.ClusterExtensionRevisionPhase + }{ + { + name: "single deploy obj", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + }, + }, + }, + }, + { + name: "all phases", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apiregistration.k8s.io/v1", + "kind": "APIService", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "some.api/v1", + "kind": "SomeCustomResource", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "PersistentVolume", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseNamespaces), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + }, + }, + }, + }, + }, + { + Name: string(applier.PhasePolicies), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseRBAC), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseCRDs), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseStorage), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "PersistentVolume", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "some.api/v1", + "kind": "SomeCustomResource", + }, + }, + }, + }, + }, + { + Name: string(applier.PhasePublish), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apiregistration.k8s.io/v1", + "kind": "APIService", + }, + }, + }, + }, + }, + }, + }, + { + name: "sorted and batched", + objs: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ServiceAccount", + }, + }, + }, + }, + want: []v1.ClusterExtensionRevisionPhase{ + { + Name: string(applier.PhaseRBAC), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ServiceAccount", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseDeploy), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + }, + }, + }, + }, + }, + }, + }, + { + name: "no objects", + objs: []v1.ClusterExtensionRevisionObject{}, + want: []v1.ClusterExtensionRevisionPhase{}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, applier.PhaseSort(tt.objs)) + }) + } +} diff --git a/internal/operator-controller/applier/preflight.go b/internal/operator-controller/applier/preflight.go new file mode 100644 index 0000000000..7b5d91e279 --- /dev/null +++ b/internal/operator-controller/applier/preflight.go @@ -0,0 +1,54 @@ +package applier + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" +) + +const ( + StateNeedsInstall string = "NeedsInstall" + StateNeedsUpgrade string = "NeedsUpgrade" + StateUnchanged string = "Unchanged" + StateError string = "Error" + maxHelmReleaseHistory = 10 +) + +// Preflight is a check that should be run before making any changes to the cluster +type Preflight interface { + // Install runs checks that should be successful prior + // to installing the Helm release. It is provided + // a Helm release and returns an error if the + // check is unsuccessful + Install(context.Context, []client.Object) error + + // Upgrade runs checks that should be successful prior + // to upgrading the Helm release. It is provided + // a Helm release and returns an error if the + // check is unsuccessful + Upgrade(context.Context, []client.Object) error +} + +// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND +// if it is set to enforcement None. +func shouldSkipPreflight(ctx context.Context, preflight Preflight, ext *ocv1.ClusterExtension, state string) bool { + l := log.FromContext(ctx) + hasCRDUpgradeSafety := ext.Spec.Install != nil && ext.Spec.Install.Preflight != nil && ext.Spec.Install.Preflight.CRDUpgradeSafety != nil + _, isCRDUpgradeSafetyInstance := preflight.(*crdupgradesafety.Preflight) + + if hasCRDUpgradeSafety && isCRDUpgradeSafetyInstance { + if state == StateNeedsInstall || state == StateNeedsUpgrade { + l.Info("crdUpgradeSafety ", "policy", ext.Spec.Install.Preflight.CRDUpgradeSafety.Enforcement) + } + if ext.Spec.Install.Preflight.CRDUpgradeSafety.Enforcement == ocv1.CRDUpgradeSafetyEnforcementNone { + // Skip this preflight check because it is of type *crdupgradesafety.Preflight and the CRD Upgrade Safety + // policy is set to None + return true + } + } + return false +} diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go new file mode 100644 index 0000000000..ffb5eb559b --- /dev/null +++ b/internal/operator-controller/applier/provider.go @@ -0,0 +1,150 @@ +package applier + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io/fs" + + "helm.sh/helm/v3/pkg/chart" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" +) + +// ManifestProvider returns the manifests that should be applied by OLM given a bundle and its associated ClusterExtension +type ManifestProvider interface { + // Get returns a set of resource manifests in bundle that take into account the configuration in ext + Get(bundle fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) +} + +// RegistryV1ManifestProvider generates the manifests that should be installed for a registry+v1 bundle +// given the user specified configuration given by the ClusterExtension API surface +type RegistryV1ManifestProvider struct { + BundleRenderer render.BundleRenderer + CertificateProvider render.CertificateProvider + IsWebhookSupportEnabled bool + IsSingleOwnNamespaceEnabled bool +} + +func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { + rv1, err := source.FromFS(bundleFS).GetBundle() + if err != nil { + return nil, err + } + + if len(rv1.CSV.Spec.APIServiceDefinitions.Owned) > 0 { + return nil, fmt.Errorf("unsupported bundle: apiServiceDefintions are not supported") + } + + if len(rv1.CSV.Spec.WebhookDefinitions) > 0 { + if !r.IsWebhookSupportEnabled { + return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported") + } else if r.CertificateProvider == nil { + return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported: certificate provider is nil") + } + } + + installModes := sets.New(rv1.CSV.Spec.InstallModes...) + if !r.IsSingleOwnNamespaceEnabled && !installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) { + return nil, fmt.Errorf("unsupported bundle: bundle does not support AllNamespaces install mode") + } + + if !installModes.HasAny( + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, + ) { + return nil, fmt.Errorf("unsupported bundle: bundle must support at least one of [AllNamespaces SingleNamespace OwnNamespace] install modes") + } + + opts := []render.Option{ + render.WithCertificateProvider(r.CertificateProvider), + } + + if r.IsSingleOwnNamespaceEnabled { + bundleConfigBytes := extensionConfigBytes(ext) + // treat no config as empty to properly validate the configuration + // e.g. ensure that validation catches missing required fields + if bundleConfigBytes == nil { + bundleConfigBytes = []byte(`{}`) + } + bundleConfig, err := bundle.UnmarshalConfig(bundleConfigBytes, rv1, ext.Spec.Namespace) + if err != nil { + return nil, fmt.Errorf("invalid bundle configuration: %w", err) + } + + if bundleConfig != nil && bundleConfig.WatchNamespace != nil { + opts = append(opts, render.WithTargetNamespaces(*bundleConfig.WatchNamespace)) + } + } + + return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) +} + +// RegistryV1HelmChartProvider creates a Helm-Chart from a registry+v1 bundle and its associated ClusterExtension +type RegistryV1HelmChartProvider struct { + ManifestProvider ManifestProvider +} + +func (r *RegistryV1HelmChartProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { + objs, err := r.ManifestProvider.Get(bundleFS, ext) + if err != nil { + return nil, err + } + + chrt := &chart.Chart{Metadata: &chart.Metadata{}} + // The need to get the underlying bundle in order to extract its annotations + // will go away once with have a bundle interface that can surface the annotations independently of the + // underlying bundle format... + rv1, err := source.FromFS(bundleFS).GetBundle() + if err != nil { + return nil, err + } + chrt.Metadata.Annotations = rv1.CSV.GetAnnotations() + for _, obj := range objs { + jsonData, err := json.Marshal(obj) + if err != nil { + return nil, err + } + hash := sha256.Sum256(jsonData) + name := fmt.Sprintf("object-%x.json", hash[0:8]) + + // Some registry+v1 manifests may actually contain Go Template strings + // that are meant to survive and actually persist into etcd (e.g. to be + // used as a templated configuration for another component). In order to + // avoid applying templating logic to registry+v1's static manifests, we + // create the manifests as Files, and then template those files via simple + // Templates. + chrt.Files = append(chrt.Files, &chart.File{ + Name: name, + Data: jsonData, + }) + chrt.Templates = append(chrt.Templates, &chart.File{ + Name: name, + Data: []byte(fmt.Sprintf(`{{.Files.Get "%s"}}`, name)), + }) + } + + return chrt, nil +} + +// ExtensionConfigBytes returns the ClusterExtension configuration input by the user +// through .spec.config as a byte slice. +func extensionConfigBytes(ext *ocv1.ClusterExtension) []byte { + if ext.Spec.Config != nil { + switch ext.Spec.Config.ConfigType { + case ocv1.ClusterExtensionConfigTypeInline: + if ext.Spec.Config.Inline != nil { + return ext.Spec.Config.Inline.Raw + } + } + } + return nil +} diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go new file mode 100644 index 0000000000..4ec20beadb --- /dev/null +++ b/internal/operator-controller/applier/provider_test.go @@ -0,0 +1,501 @@ +package applier_test + +import ( + "errors" + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/applier" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" + . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" +) + +func Test_RegistryV1ManifestProvider_Integration(t *testing.T) { + t.Run("surfaces bundle source errors", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{} + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + _, err := provider.Get(fstest.MapFS{}, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "metadata/annotations.yaml: file does not exist") + }) + + t.Run("surfaces bundle renderer errors", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + return nil, errors.New("some error") + }, + }, + }, + } + + // The contents of the bundle are not important for this tesy, only that it be a valid bundle + // to avoid errors in the deserialization process + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "some error") + }) + + t.Run("surfaces bundle config unmarshall errors", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + return nil, nil + }, + }, + }, + // must be true for now as we only unmarshal configuration when this feature is on + // once we go GA and remove IsSingleOwnNamespaceEnabled it's ok to just delete this + IsSingleOwnNamespaceEnabled: true, + } + + // The contents of the bundle are not important for this tesy, only that it be a valid bundle + // to avoid errors in the deserialization process + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "install-namespace"}`), + }, + }, + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid bundle configuration") + }) + + t.Run("returns rendered manifests", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + }, + }).Build() + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + objs, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + + exp := ToUnstructuredT(t, &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "install-namespace", + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{}, + }, + }) + + require.Equal(t, []client.Object{exp}, objs) + }) +} + +func Test_RegistryV1ManifestProvider_APIServiceSupport(t *testing.T) { + t.Run("rejects registry+v1 bundles with API service definitions", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{} + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithOwnedAPIServiceDescriptions(v1alpha1.APIServiceDescription{Name: "test-apiservice"}).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported bundle: apiServiceDefintions are not supported") + }) +} + +func Test_RegistryV1ManifestProvider_WebhookSupport(t *testing.T) { + t.Run("rejects bundles with webhook definitions if support is disabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsWebhookSupportEnabled: false, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithWebhookDefinitions(v1alpha1.WebhookDescription{}).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "webhookDefinitions are not supported") + }) + + t.Run("fails if bundle contains webhook definitions, webhook support is enabled, but the certificate provider is undefined", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsWebhookSupportEnabled: true, + CertificateProvider: nil, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithWebhookDefinitions(v1alpha1.WebhookDescription{}).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "webhookDefinitions are not supported") + }) + + t.Run("accepts bundles with webhook definitions if support is enabled and a certificate provider is defined", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + CertificateProvider: FakeCertProvider{}, + IsWebhookSupportEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV( + clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions(v1alpha1.WebhookDescription{}).Build()). + Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + }) +} + +func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { + t.Run("rejects bundles without AllNamespaces install mode when Single/OwnNamespace install mode support is disabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: false, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Equal(t, "unsupported bundle: bundle does not support AllNamespaces install mode", err.Error()) + }) + + t.Run("rejects bundles without AllNamespaces install mode and with SingleNamespace support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { + expectedWatchNamespace := "some-namespace" + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: false, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "` + expectedWatchNamespace + `"}`), + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported bundle") + }) + + t.Run("rejects bundles without AllNamespaces install mode and with OwnNamespace support when Single/OwnNamespace install mode support is disabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: false, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported bundle") + }) + + t.Run("accepts bundles with install modes {SingleNamespace} when the appropriate configuration is given", func(t *testing.T) { + expectedWatchNamespace := "some-namespace" + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure watch namespace is appropriately configured") + require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "` + expectedWatchNamespace + `"}`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("rejects bundles with {SingleNamespace} install modes when no configuration is given", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing") + }) + + t.Run("accepts bundles with {OwnNamespace} install modes when the appropriate configuration is given", func(t *testing.T) { + installNamespace := "some-namespace" + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure watch namespace is appropriately configured") + require.Equal(t, []string{installNamespace}, opts.TargetNamespaces) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: installNamespace, + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "` + installNamespace + `"}`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("rejects bundles with {OwnNamespace} install modes when no configuration is given", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing") + }) + + t.Run("rejects bundles with {OwnNamespace} install modes when watchNamespace is not install namespace", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "not-install-namespace"}`), + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid 'watchNamespace' \"not-install-namespace\": must be install namespace (install-namespace)") + }) + + t.Run("rejects bundles without AllNamespaces, SingleNamespace, or OwnNamespace install mode support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeMultiNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Equal(t, "unsupported bundle: bundle must support at least one of [AllNamespaces SingleNamespace OwnNamespace] install modes", err.Error()) + }) +} + +func Test_RegistryV1HelmChartProvider_Integration(t *testing.T) { + t.Run("surfaces bundle source errors", func(t *testing.T) { + provider := applier.RegistryV1HelmChartProvider{ + ManifestProvider: DummyManifestProvider, + } + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + _, err := provider.Get(fstest.MapFS{}, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "metadata/annotations.yaml: file does not exist") + }) + + t.Run("surfaces manifest provider failures", func(t *testing.T) { + provider := applier.RegistryV1HelmChartProvider{ + ManifestProvider: &FakeManifestProvider{ + GetFn: func(bundle fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { + return nil, errors.New("some error") + }, + }, + } + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + _, err := provider.Get(fstest.MapFS{}, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "some error") + }) +} + +func Test_RegistryV1HelmChartProvider_Chart(t *testing.T) { + provider := applier.RegistryV1HelmChartProvider{ + ManifestProvider: &applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + }, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV( + clusterserviceversion.Builder(). + WithAnnotations(map[string]string{"foo": "bar"}). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + Build()). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "testService", + }, + }).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + chart, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + require.NotNil(t, chart) + require.NotNil(t, chart.Metadata) + + t.Log("Check Chart metadata contains CSV annotations") + require.Equal(t, map[string]string{"foo": "bar"}, chart.Metadata.Annotations) + + t.Log("Check Chart templates have the same number of resources generated by the renderer") + require.Len(t, chart.Templates, 1) +} + +var DummyManifestProvider = &FakeManifestProvider{ + GetFn: func(bundle fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { + return []client.Object{}, nil + }, +} + +type FakeManifestProvider struct { + GetFn func(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) +} + +func (f *FakeManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { + return f.GetFn(bundleFS, ext) +} diff --git a/internal/operator-controller/applier/watchnamespace.go b/internal/operator-controller/applier/watchnamespace.go deleted file mode 100644 index 193f456b3c..0000000000 --- a/internal/operator-controller/applier/watchnamespace.go +++ /dev/null @@ -1,32 +0,0 @@ -package applier - -import ( - "fmt" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/validation" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/internal/operator-controller/features" -) - -const ( - AnnotationClusterExtensionWatchNamespace = "olm.operatorframework.io/watch-namespace" -) - -// GetWatchNamespace determines the watch namespace the ClusterExtension should use -// Note: this is a temporary artifice to enable gated use of single/own namespace install modes -// for registry+v1 bundles. This will go away once the ClusterExtension API is updated to include -// (opaque) runtime configuration. -func GetWatchNamespace(ext *ocv1.ClusterExtension) (string, error) { - if features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport) { - if ext != nil && ext.Annotations[AnnotationClusterExtensionWatchNamespace] != "" { - watchNamespace := ext.Annotations[AnnotationClusterExtensionWatchNamespace] - if errs := validation.IsDNS1123Subdomain(watchNamespace); len(errs) > 0 { - return "", fmt.Errorf("invalid watch namespace '%s': namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", watchNamespace) - } - return ext.Annotations[AnnotationClusterExtensionWatchNamespace], nil - } - } - return corev1.NamespaceAll, nil -} diff --git a/internal/operator-controller/applier/watchnamespace_test.go b/internal/operator-controller/applier/watchnamespace_test.go deleted file mode 100644 index 90e018dc7c..0000000000 --- a/internal/operator-controller/applier/watchnamespace_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package applier_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - featuregatetesting "k8s.io/component-base/featuregate/testing" - - v1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/internal/operator-controller/applier" - "github.com/operator-framework/operator-controller/internal/operator-controller/features" -) - -func TestGetWatchNamespacesWhenFeatureGateIsDisabled(t *testing.T) { - watchNamespace, err := applier.GetWatchNamespace(&v1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace", - }, - }, - Spec: v1.ClusterExtensionSpec{}, - }) - require.NoError(t, err) - t.Log("Check watchNamespace is '' even if the annotation is set") - require.Equal(t, corev1.NamespaceAll, watchNamespace) -} - -func TestGetWatchNamespace(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.SingleOwnNamespaceInstallSupport, true) - - for _, tt := range []struct { - name string - want string - csv *v1.ClusterExtension - expectError bool - }{ - { - name: "cluster extension does not have watch namespace annotation", - want: corev1.NamespaceAll, - csv: &v1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - Annotations: nil, - }, - Spec: v1.ClusterExtensionSpec{}, - }, - expectError: false, - }, { - name: "cluster extension has valid namespace annotation", - want: "watch-namespace", - csv: &v1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace", - }, - }, - Spec: v1.ClusterExtensionSpec{}, - }, - expectError: false, - }, { - name: "cluster extension has invalid namespace annotation: multiple watch namespaces", - want: "", - csv: &v1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace,watch-namespace2,watch-namespace3", - }, - }, - Spec: v1.ClusterExtensionSpec{}, - }, - expectError: true, - }, { - name: "cluster extension has invalid namespace annotation: invalid name", - want: "", - csv: &v1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace-", - }, - }, - Spec: v1.ClusterExtensionSpec{}, - }, - expectError: true, - }, - } { - t.Run(tt.name, func(t *testing.T) { - got, err := applier.GetWatchNamespace(tt.csv) - require.Equal(t, tt.want, got) - require.Equal(t, tt.expectError, err != nil) - }) - } -} diff --git a/internal/operator-controller/conditionsets/conditionsets.go b/internal/operator-controller/conditionsets/conditionsets.go index c69aff4219..0d63e1abb2 100644 --- a/internal/operator-controller/conditionsets/conditionsets.go +++ b/internal/operator-controller/conditionsets/conditionsets.go @@ -39,4 +39,6 @@ var ConditionReasons = []string{ ocv1.ReasonFailed, ocv1.ReasonBlocked, ocv1.ReasonRetrying, + ocv1.ReasonAbsent, + ocv1.ReasonRolloutInProgress, } diff --git a/internal/operator-controller/contentmanager/source/dynamicsource.go b/internal/operator-controller/contentmanager/source/dynamicsource.go index dc252492cb..9a4e6910db 100644 --- a/internal/operator-controller/contentmanager/source/dynamicsource.go +++ b/internal/operator-controller/contentmanager/source/dynamicsource.go @@ -120,7 +120,7 @@ func (dis *dynamicInformerSource) Start(ctx context.Context, q workqueue.TypedRa dis.cfg.OnPostSyncError(dis.informerCtx) } dis.informerCancel() - cgocache.DefaultWatchErrorHandler(r, err) + cgocache.DefaultWatchErrorHandler(dis.informerCtx, r, err) }) if err != nil { return fmt.Errorf("setting WatchErrorHandler: %w", err) diff --git a/internal/operator-controller/controllers/clustercatalog_controller.go b/internal/operator-controller/controllers/clustercatalog_controller.go index da882100ee..0654d83e7a 100644 --- a/internal/operator-controller/controllers/clustercatalog_controller.go +++ b/internal/operator-controller/controllers/clustercatalog_controller.go @@ -45,8 +45,6 @@ type ClusterCatalogReconciler struct { CatalogCachePopulator CatalogCachePopulator } -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=get;list;watch - func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("cluster-catalog") ctx = log.IntoContext(ctx, l) @@ -55,7 +53,7 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque defer l.Info("reconcile ending") existingCatalog := &ocv1.ClusterCatalog{} - err := r.Client.Get(ctx, req.NamespacedName, existingCatalog) + err := r.Get(ctx, req.NamespacedName, existingCatalog) if apierrors.IsNotFound(err) { if err := r.CatalogCache.Remove(req.Name); err != nil { return ctrl.Result{}, fmt.Errorf("error removing cache for catalog %q: %v", req.Name, err) diff --git a/internal/operator-controller/controllers/clusterextension_admission_test.go b/internal/operator-controller/controllers/clusterextension_admission_test.go index 3bf58fd489..38c6c60d41 100644 --- a/internal/operator-controller/controllers/clusterextension_admission_test.go +++ b/internal/operator-controller/controllers/clusterextension_admission_test.go @@ -6,13 +6,14 @@ import ( "testing" "github.com/stretchr/testify/require" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ocv1 "github.com/operator-framework/operator-controller/api/v1" ) func TestClusterExtensionSourceConfig(t *testing.T) { - sourceTypeEmptyError := "Invalid value: \"null\"" + sourceTypeEmptyError := "Invalid value: null" sourceTypeMismatchError := "spec.source.sourceType: Unsupported value" sourceConfigInvalidError := "spec.source: Invalid value" // unionField represents the required Catalog or (future) Bundle field required by SourceConfig @@ -448,6 +449,54 @@ func TestClusterExtensionAdmissionInstall(t *testing.T) { } } +func Test_ClusterExtensionAdmissionInlineConfig(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + configBytes []byte + errMsg string + }{ + { + name: "rejects valid json that is not of an object type", + configBytes: []byte(`true`), + errMsg: "spec.config.inline in body must be of type object", + }, + { + name: "accepts valid json object", + configBytes: []byte(`{"key": "value"}`), + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cl := newClient(t) + err := cl.Create(context.Background(), buildClusterExtension(ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "package", + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "default", + }, + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: tc.configBytes, + }, + }, + })) + if tc.errMsg == "" { + require.NoError(t, err, "unexpected error for inline bundle configuration %q: %w", string(tc.configBytes), err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + func buildClusterExtension(spec ocv1.ClusterExtensionSpec) *ocv1.ClusterExtension { return &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index e571174b09..7bcedde656 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -17,12 +17,13 @@ limitations under the License. package controllers import ( + "cmp" "context" "errors" "fmt" "io/fs" + "slices" "strings" - "time" "github.com/go-logr/logr" "helm.sh/helm/v3/pkg/release" @@ -34,7 +35,6 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" @@ -52,7 +52,6 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" - "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" @@ -71,49 +70,41 @@ type ClusterExtensionReconciler struct { ImageCache imageutil.Cache ImagePuller imageutil.Puller - Applier Applier - Manager contentmanager.Manager - controller crcontroller.Controller - cache cache.Cache - InstalledBundleGetter InstalledBundleGetter - Finalizers crfinalizer.Finalizers + StorageMigrator StorageMigrator + Applier Applier + RevisionStatesGetter RevisionStatesGetter + Finalizers crfinalizer.Finalizers +} + +type StorageMigrator interface { + Migrate(context.Context, *ocv1.ClusterExtension, map[string]string) error } type Applier interface { // Apply applies the content in the provided fs.FS using the configuration of the provided ClusterExtension. // It also takes in a map[string]string to be applied to all applied resources as labels and another // map[string]string used to create a unique identifier for a stored reference to the resources created. - Apply(context.Context, fs.FS, *ocv1.ClusterExtension, map[string]string, map[string]string) ([]client.Object, string, error) + Apply(context.Context, fs.FS, *ocv1.ClusterExtension, map[string]string, map[string]string) (bool, string, error) } -type InstalledBundleGetter interface { - GetInstalledBundle(ctx context.Context, ext *ocv1.ClusterExtension) (*InstalledBundle, error) +type RevisionStatesGetter interface { + GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*RevisionStates, error) } -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/status,verbs=update;patch -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update -//+kubebuilder:rbac:namespace=system,groups=core,resources=secrets,verbs=create;update;patch;delete;deletecollection;get;list;watch -//+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create -//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=list;watch - -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=list;watch - // The operator controller needs to watch all the bundle objects and reconcile accordingly. Though not ideal, but these permissions are required. // This has been taken from rukpak, and an issue was created before to discuss it: https://github.com/operator-framework/rukpak/issues/800. func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("cluster-extension") ctx = log.IntoContext(ctx, l) - l.Info("reconcile starting") - defer l.Info("reconcile ending") - existingExt := &ocv1.ClusterExtension{} - if err := r.Client.Get(ctx, req.NamespacedName, existingExt); err != nil { + if err := r.Get(ctx, req.NamespacedName, existingExt); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + l.Info("reconcile starting") + defer l.Info("reconcile ending") + reconciledExt := existingExt.DeepCopy() res, reconcileErr := r.reconcile(ctx, reconciledExt) @@ -122,7 +113,7 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req updateFinalizers := !equality.Semantic.DeepEqual(existingExt.Finalizers, reconciledExt.Finalizers) // If any unexpected fields have changed, panic before updating the resource - unexpectedFieldsChanged := checkForUnexpectedFieldChange(*existingExt, *reconciledExt) + unexpectedFieldsChanged := checkForUnexpectedClusterExtensionFieldChange(*existingExt, *reconciledExt) if unexpectedFieldsChanged { panic("spec or metadata changed by reconciler") } @@ -140,7 +131,7 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req reconciledExt.Finalizers = finalizers if updateFinalizers { - if err := r.Client.Update(ctx, reconciledExt); err != nil { + if err := r.Update(ctx, reconciledExt); err != nil { reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err)) } } @@ -155,21 +146,19 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C cond := apimeta.FindStatusCondition(ext.Status.Conditions, condType) if cond == nil { // Create a new condition with a valid reason and add it - newCond := metav1.Condition{ + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: condType, Status: metav1.ConditionFalse, Reason: string(reason), Message: message, ObservedGeneration: ext.GetGeneration(), - LastTransitionTime: metav1.NewTime(time.Now()), - } - ext.Status.Conditions = append(ext.Status.Conditions, newCond) + }) } } } // Compare resources - ignoring status & metadata.finalizers -func checkForUnexpectedFieldChange(a, b ocv1.ClusterExtension) bool { +func checkForUnexpectedClusterExtensionFieldChange(a, b ocv1.ClusterExtension) bool { a.Status, b.Status = ocv1.ClusterExtensionStatus{}, ocv1.ClusterExtensionStatus{} a.Finalizers, b.Finalizers = []string{}, []string{} return !equality.Semantic.DeepEqual(a, b) @@ -206,8 +195,27 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl return ctrl.Result{}, nil } + if ext.GetDeletionTimestamp() != nil { + // If we've gotten here, that means the cluster extension is being deleted, we've handled all of + // _our_ finalizers (above), but the cluster extension is still present in the cluster, likely + // because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan + // deletion finalizer). + return ctrl.Result{}, nil + } + + objLbls := map[string]string{ + labels.OwnerKindKey: ocv1.ClusterExtensionKind, + labels.OwnerNameKey: ext.GetName(), + } + + if r.StorageMigrator != nil { + if err := r.StorageMigrator.Migrate(ctx, ext, objLbls); err != nil { + return ctrl.Result{}, fmt.Errorf("migrating storage: %w", err) + } + } + l.Info("getting installed bundle") - installedBundle, err := r.InstalledBundleGetter.GetInstalledBundle(ctx, ext) + revisionStates, err := r.RevisionStatesGetter.GetRevisionStates(ctx, ext) if err != nil { setInstallStatus(ext, nil) var saerr *authentication.ServiceAccountNotFoundError @@ -221,60 +229,62 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl return ctrl.Result{}, err } - // run resolution - l.Info("resolving bundle") - var bm *ocv1.BundleMetadata - if installedBundle != nil { - bm = &installedBundle.BundleMetadata - } - resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolver.Resolve(ctx, ext, bm) - if err != nil { - // Note: We don't distinguish between resolution-specific errors and generic errors - setStatusProgressing(ext, err) - setInstalledStatusFromBundle(ext, installedBundle) - ensureAllConditionsWithReason(ext, ocv1.ReasonFailed, err.Error()) - return ctrl.Result{}, err - } + var resolvedRevisionMetadata *RevisionMetadata + if len(revisionStates.RollingOut) == 0 { + l.Info("resolving bundle") + var bm *ocv1.BundleMetadata + if revisionStates.Installed != nil { + bm = &revisionStates.Installed.BundleMetadata + } + resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolver.Resolve(ctx, ext, bm) + if err != nil { + // Note: We don't distinguish between resolution-specific errors and generic errors + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, revisionStates) + ensureAllConditionsWithReason(ext, ocv1.ReasonFailed, err.Error()) + return ctrl.Result{}, err + } - // set deprecation status after _successful_ resolution - // TODO: - // 1. It seems like deprecation status should reflect the currently installed bundle, not the resolved - // bundle. So perhaps we should set package and channel deprecations directly after resolution, but - // defer setting the bundle deprecation until we successfully install the bundle. - // 2. If resolution fails because it can't find a bundle, that doesn't mean we wouldn't be able to find - // a deprecation for the ClusterExtension's spec.packageName. Perhaps we should check for a non-nil - // resolvedDeprecation even if resolution returns an error. If present, we can still update some of - // our deprecation status. - // - Open question though: what if different catalogs have different opinions of what's deprecated. - // If we can't resolve a bundle, how do we know which catalog to trust for deprecation information? - // Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set - // the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from - // all catalogs? - SetDeprecationStatus(ext, resolvedBundle.Name, resolvedDeprecation) - - resolvedBundleMetadata := bundleutil.MetadataFor(resolvedBundle.Name, *resolvedBundleVersion) + // set deprecation status after _successful_ resolution + // TODO: + // 1. It seems like deprecation status should reflect the currently installed bundle, not the resolved + // bundle. So perhaps we should set package and channel deprecations directly after resolution, but + // defer setting the bundle deprecation until we successfully install the bundle. + // 2. If resolution fails because it can't find a bundle, that doesn't mean we wouldn't be able to find + // a deprecation for the ClusterExtension's spec.packageName. Perhaps we should check for a non-nil + // resolvedDeprecation even if resolution returns an error. If present, we can still update some of + // our deprecation status. + // - Open question though: what if different catalogs have different opinions of what's deprecated. + // If we can't resolve a bundle, how do we know which catalog to trust for deprecation information? + // Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set + // the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from + // all catalogs? + SetDeprecationStatus(ext, resolvedBundle.Name, resolvedDeprecation) + resolvedRevisionMetadata = &RevisionMetadata{ + Package: resolvedBundle.Package, + Image: resolvedBundle.Image, + BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, *resolvedBundleVersion), + } + } else { + resolvedRevisionMetadata = revisionStates.RollingOut[0] + } l.Info("unpacking resolved bundle") - imageFS, _, _, err := r.ImagePuller.Pull(ctx, ext.GetName(), resolvedBundle.Image, r.ImageCache) + imageFS, _, _, err := r.ImagePuller.Pull(ctx, ext.GetName(), resolvedRevisionMetadata.Image, r.ImageCache) if err != nil { // Wrap the error passed to this with the resolution information until we have successfully // installed since we intend for the progressing condition to replace the resolved condition // and will be removing the .status.resolution field from the ClusterExtension status API - setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) - setInstalledStatusFromBundle(ext, installedBundle) + setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err)) + setInstalledStatusFromRevisionStates(ext, revisionStates) return ctrl.Result{}, err } - objLbls := map[string]string{ - labels.OwnerKindKey: ocv1.ClusterExtensionKind, - labels.OwnerNameKey: ext.GetName(), - } - storeLbls := map[string]string{ - labels.BundleNameKey: resolvedBundle.Name, - labels.PackageNameKey: resolvedBundle.Package, - labels.BundleVersionKey: resolvedBundleVersion.String(), - labels.BundleReferenceKey: resolvedBundle.Image, + labels.BundleNameKey: resolvedRevisionMetadata.Name, + labels.PackageNameKey: resolvedRevisionMetadata.Package, + labels.BundleVersionKey: resolvedRevisionMetadata.Version, + labels.BundleReferenceKey: resolvedRevisionMetadata.Image, } l.Info("applying bundle contents") @@ -287,41 +297,32 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl // to ensure exponential backoff can occur: // - Permission errors (it is not possible to watch changes to permissions. // The only way to eventually recover from permission errors is to keep retrying). - managedObjs, _, err := r.Applier.Apply(ctx, imageFS, ext, objLbls, storeLbls) - if err != nil { - setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err)) - // Now that we're actually trying to install, use the error - setInstalledStatusFromBundle(ext, installedBundle) - return ctrl.Result{}, err - } + rolloutSucceeded, rolloutStatus, err := r.Applier.Apply(ctx, imageFS, ext, objLbls, storeLbls) - newInstalledBundle := &InstalledBundle{ - BundleMetadata: resolvedBundleMetadata, - Image: resolvedBundle.Image, + // Set installed status + if rolloutSucceeded { + revisionStates = &RevisionStates{Installed: resolvedRevisionMetadata} + } else if err == nil && revisionStates.Installed == nil && len(revisionStates.RollingOut) == 0 { + revisionStates = &RevisionStates{RollingOut: []*RevisionMetadata{resolvedRevisionMetadata}} } - // Successful install - setInstalledStatusFromBundle(ext, newInstalledBundle) + setInstalledStatusFromRevisionStates(ext, revisionStates) - l.Info("watching managed objects") - cache, err := r.Manager.Get(ctx, ext) + // If there was an error applying the resolved bundle, + // report the error via the Progressing condition. if err != nil { - // No need to wrap error with resolution information here (or beyond) since the - // bundle was successfully installed and the information will be present in - // the .status.installed field - setStatusProgressing(ext, err) - return ctrl.Result{}, err - } - - if err := cache.Watch(ctx, r.controller, managedObjs...); err != nil { - setStatusProgressing(ext, err) + setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err)) return ctrl.Result{}, err + } else if !rolloutSucceeded { + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRolloutInProgress, + Message: rolloutStatus, + ObservedGeneration: ext.GetGeneration(), + }) + } else { + setStatusProgressing(ext, nil) } - - // If we made it here, we have successfully reconciled the ClusterExtension - // and have reached the desired state. Since the Progressing status should reflect - // our progress towards the desired state, we also set it when we have reached - // the desired state by providing a nil error value. - setStatusProgressing(ext, nil) return ctrl.Result{}, nil } @@ -372,7 +373,7 @@ func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, depreca if len(deprecationMessages) > 0 { status, reason, message = metav1.ConditionTrue, ocv1.ReasonDeprecated, strings.Join(deprecationMessages, ";") } - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeDeprecated, Reason: reason, Status: status, @@ -394,7 +395,7 @@ func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, depreca message = fmt.Sprintf("%s\n%s", message, entry.Message) } } - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: conditionType, Reason: reason, Status: status, @@ -404,9 +405,17 @@ func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, depreca } } +type ControllerBuilderOption func(builder *ctrl.Builder) + +func WithOwns(obj client.Object) ControllerBuilderOption { + return func(builder *ctrl.Builder) { + builder.Owns(obj) + } +} + // SetupWithManager sets up the controller with the Manager. -func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { - controller, err := ctrl.NewControllerManagedBy(mgr). +func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager, opts ...ControllerBuilderOption) (crcontroller.Controller, error) { + ctrlBuilder := ctrl.NewControllerManagedBy(mgr). For(&ocv1.ClusterExtension{}). Named("controller-operator-cluster-extension-controller"). Watches(&ocv1.ClusterCatalog{}, @@ -427,15 +436,13 @@ func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { } return true }, - })). - Build(r) - if err != nil { - return err + })) + + for _, applyOpt := range opts { + applyOpt(ctrlBuilder) } - r.controller = controller - r.cache = mgr.GetCache() - return nil + return ctrlBuilder.Build(r) } func wrapErrorWithResolutionInfo(resolved ocv1.BundleMetadata, err error) error { @@ -466,16 +473,22 @@ func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) crh } } -type DefaultInstalledBundleGetter struct { - helmclient.ActionClientGetter +type RevisionMetadata struct { + Package string + Image string + ocv1.BundleMetadata } -type InstalledBundle struct { - ocv1.BundleMetadata - Image string +type RevisionStates struct { + Installed *RevisionMetadata + RollingOut []*RevisionMetadata } -func (d *DefaultInstalledBundleGetter) GetInstalledBundle(ctx context.Context, ext *ocv1.ClusterExtension) (*InstalledBundle, error) { +type HelmRevisionStatesGetter struct { + helmclient.ActionClientGetter +} + +func (d *HelmRevisionStatesGetter) GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*RevisionStates, error) { cl, err := d.ActionClientFor(ctx, ext) if err != nil { return nil, err @@ -485,22 +498,75 @@ func (d *DefaultInstalledBundleGetter) GetInstalledBundle(ctx context.Context, e if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { return nil, err } + rs := &RevisionStates{} if len(relhis) == 0 { - return nil, nil + return rs, nil } // relhis[0].Info.Status is the status of the most recent install attempt. // But we need to look for the most-recent _Deployed_ release for _, rel := range relhis { if rel.Info != nil && rel.Info.Status == release.StatusDeployed { - return &InstalledBundle{ + rs.Installed = &RevisionMetadata{ + Package: rel.Labels[labels.PackageNameKey], + Image: rel.Labels[labels.BundleReferenceKey], BundleMetadata: ocv1.BundleMetadata{ Name: rel.Labels[labels.BundleNameKey], Version: rel.Labels[labels.BundleVersionKey], }, - Image: rel.Labels[labels.BundleReferenceKey], - }, nil + } + break } } - return nil, nil + return rs, nil +} + +type BoxcutterRevisionStatesGetter struct { + Reader client.Reader +} + +func (d *BoxcutterRevisionStatesGetter) GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*RevisionStates, error) { + // TODO: boxcutter applier has a nearly identical bit of code for listing and sorting revisions + // only difference here is that it sorts in reverse order to start iterating with the most + // recent revisions. We should consolidate to avoid code duplication. + existingRevisionList := &ocv1.ClusterExtensionRevisionList{} + if err := d.Reader.List(ctx, existingRevisionList, client.MatchingLabels{ + ClusterExtensionRevisionOwnerLabel: ext.Name, + }); err != nil { + return nil, fmt.Errorf("listing revisions: %w", err) + } + slices.SortFunc(existingRevisionList.Items, func(a, b ocv1.ClusterExtensionRevision) int { + return cmp.Compare(a.Spec.Revision, b.Spec.Revision) + }) + + rs := &RevisionStates{} + for _, rev := range existingRevisionList.Items { + switch rev.Spec.LifecycleState { + case ocv1.ClusterExtensionRevisionLifecycleStateActive, + ocv1.ClusterExtensionRevisionLifecycleStatePaused: + default: + // Skip anything not active or paused, which should only be "Archived". + continue + } + + // TODO: the setting of these annotations (happens in boxcutter applier when we pass in "storageLabels") + // is fairly decoupled from this code where we get the annotations back out. We may want to co-locate + // the set/get logic a bit better to make it more maintainable and less likely to get out of sync. + rm := &RevisionMetadata{ + Package: rev.Labels[labels.PackageNameKey], + Image: rev.Annotations[labels.BundleReferenceKey], + BundleMetadata: ocv1.BundleMetadata{ + Name: rev.Annotations[labels.BundleNameKey], + Version: rev.Annotations[labels.BundleVersionKey], + }, + } + + if apimeta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) { + rs.Installed = rm + } else { + rs.RollingOut = append(rs.RollingOut, rm) + } + } + + return rs, nil } diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index be61891a0f..437f62dcec 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -48,6 +48,79 @@ func TestClusterExtensionDoesNotExist(t *testing.T) { require.NoError(t, err) } +func TestClusterExtensionShortCircuitsReconcileDuringDeletion(t *testing.T) { + cl, reconciler := newClientAndReconciler(t) + + installedBundleGetterCalledErr := errors.New("revision states getter called") + checkInstalledBundleGetterCalled := func(t require.TestingT, err error, args ...interface{}) { + require.Equal(t, installedBundleGetterCalledErr, err) + } + reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ + Err: installedBundleGetterCalledErr, + } + + type testCase struct { + name string + finalizers []string + shouldDelete bool + expectErr require.ErrorAssertionFunc + } + for _, tc := range []testCase{ + { + name: "no finalizers, not deleted", + expectErr: checkInstalledBundleGetterCalled, + }, + { + name: "has finalizers, not deleted", + finalizers: []string{"finalizer"}, + expectErr: checkInstalledBundleGetterCalled, + }, + { + name: "has finalizers, deleted", + finalizers: []string{"finalizer"}, + shouldDelete: true, + expectErr: require.NoError, + }, + } { + t.Run(tc.name, func(t *testing.T) { + pkgName := fmt.Sprintf("test-pkg-%s", rand.String(6)) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + + t.Log("When the cluster extension specifies a non-existent package") + t.Log("By initializing cluster state") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: extKey.Name, + Finalizers: tc.finalizers, + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: pkgName, + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "default", + }, + }, + } + require.NoError(t, cl.Create(ctx, clusterExtension)) + if tc.shouldDelete { + require.NoError(t, cl.Delete(ctx, clusterExtension)) + } + + t.Log("By running reconcile") + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + tc.expectErr(t, err) + }) + } +} + func TestClusterExtensionResolutionFails(t *testing.T) { pkgName := fmt.Sprintf("non-existent-%s", rand.String(6)) cl, reconciler := newClientAndReconciler(t) @@ -275,8 +348,8 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) func TestClusterExtensionServiceAccountNotFound(t *testing.T) { cl, reconciler := newClientAndReconciler(t) - reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{ - err: &authentication.ServiceAccountNotFoundError{ + reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ + Err: &authentication.ServiceAccountNotFoundError{ ServiceAccountName: "missing-sa", ServiceAccountNamespace: "default", }} @@ -375,17 +448,16 @@ func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) { }, &v, nil, nil }) - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, - } - reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{ - bundle: &controllers.InstalledBundle{ - BundleMetadata: ocv1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + BundleMetadata: ocv1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, + Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + }, }, } reconciler.Applier = &MockApplier{ - objs: []client.Object{}, + installCompleted: true, } res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) @@ -471,10 +543,8 @@ func TestClusterExtensionManagerFailed(t *testing.T) { }, &v, nil, nil }) reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - err: errors.New("manager fail"), + installCompleted: true, + err: errors.New("manager fail"), } res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) require.Equal(t, ctrl.Result{}, res) @@ -550,12 +620,8 @@ func TestClusterExtensionManagedContentCacheWatchFail(t *testing.T) { }, &v, nil, nil }) reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{ - err: errors.New("watch error"), - }, + installCompleted: true, + err: errors.New("watch error"), } res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) require.Equal(t, ctrl.Result{}, res) @@ -630,10 +696,7 @@ func TestClusterExtensionInstallationSucceeds(t *testing.T) { }, &v, nil, nil }) reconciler.Applier = &MockApplier{ - objs: []client.Object{}, - } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, + installCompleted: true, } res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) require.Equal(t, ctrl.Result{}, res) @@ -709,15 +772,14 @@ func TestClusterExtensionDeleteFinalizerFails(t *testing.T) { fakeFinalizer := "fake.testfinalizer.io" finalizersMessage := "still have finalizers" reconciler.Applier = &MockApplier{ - objs: []client.Object{}, + installCompleted: true, } - reconciler.Manager = &MockManagedContentCacheManager{ - cache: &MockManagedContentCache{}, - } - reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{ - bundle: &controllers.InstalledBundle{ - BundleMetadata: ocv1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, - Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + reconciler.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + BundleMetadata: ocv1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}, + Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + }, }, } err = reconciler.Finalizers.Register(fakeFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) { @@ -1369,17 +1431,17 @@ func TestSetDeprecationStatus(t *testing.T) { controllers.SetDeprecationStatus(tc.clusterExtension, tc.bundle.Name, tc.deprecation) // TODO: we should test for unexpected changes to lastTransitionTime. We only expect // lastTransitionTime to change when the status of the condition changes. - assert.Equal(t, "", cmp.Diff(tc.expectedClusterExtension, tc.clusterExtension, cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime"))) + assert.Empty(t, cmp.Diff(tc.expectedClusterExtension, tc.clusterExtension, cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime"))) }) } } type MockActionGetter struct { - description string - rels []*release.Release - err error - expectedBundle *controllers.InstalledBundle - expectedError error + description string + rels []*release.Release + err error + expectedInstalled *controllers.RevisionMetadata + expectedError error } func (mag *MockActionGetter) ActionClientFor(ctx context.Context, obj client.Object) (helmclient.ActionInterface, error) { @@ -1412,7 +1474,7 @@ func (mag *MockActionGetter) Reconcile(rel *release.Release) error { } func TestGetInstalledBundleHistory(t *testing.T) { - getter := controllers.DefaultInstalledBundleGetter{} + getter := controllers.HelmRevisionStatesGetter{} ext := ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ @@ -1452,7 +1514,7 @@ func TestGetInstalledBundleHistory(t *testing.T) { }, }, nil, - &controllers.InstalledBundle{ + &controllers.RevisionMetadata{ BundleMetadata: ocv1.BundleMetadata{ Name: "test-ext", Version: "1.0", @@ -1487,7 +1549,7 @@ func TestGetInstalledBundleHistory(t *testing.T) { }, }, nil, - &controllers.InstalledBundle{ + &controllers.RevisionMetadata{ BundleMetadata: ocv1.BundleMetadata{ Name: "test-ext", Version: "1.0", @@ -1500,8 +1562,14 @@ func TestGetInstalledBundleHistory(t *testing.T) { for _, tst := range mag { t.Log(tst.description) getter.ActionClientGetter = &tst - md, err := getter.GetInstalledBundle(context.Background(), &ext) - require.Equal(t, tst.expectedError, err) - require.Equal(t, tst.expectedBundle, md) + md, err := getter.GetRevisionStates(context.Background(), &ext) + if tst.expectedError != nil { + require.Equal(t, tst.expectedError, err) + require.Nil(t, md) + } else { + require.NoError(t, err) + require.Equal(t, tst.expectedInstalled, md.Installed) + require.Nil(t, md.RollingOut) + } } } diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go new file mode 100644 index 0000000000..8882491615 --- /dev/null +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -0,0 +1,541 @@ +//go:build !standard + +package controllers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "pkg.package-operator.run/boxcutter" + "pkg.package-operator.run/boxcutter/machinery" + machinerytypes "pkg.package-operator.run/boxcutter/machinery/types" + "pkg.package-operator.run/boxcutter/probing" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +const ( + ClusterExtensionRevisionOwnerLabel = "olm.operatorframework.io/owner" + clusterExtensionRevisionTeardownFinalizer = "olm.operatorframework.io/teardown" +) + +// ClusterExtensionRevisionReconciler actions individual snapshots of ClusterExtensions, +// as part of the boxcutter integration. +type ClusterExtensionRevisionReconciler struct { + Client client.Client + RevisionEngine RevisionEngine + TrackingCache trackingCache +} + +type trackingCache interface { + client.Reader + Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source + Watch(ctx context.Context, user client.Object, gvks sets.Set[schema.GroupVersionKind]) error + Free(ctx context.Context, user client.Object) error +} + +type RevisionEngine interface { + Teardown(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) + Reconcile(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) +} + +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions,verbs=get;list;watch;update;patch;create;delete +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/status,verbs=update;patch +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensionrevisions/finalizers,verbs=update + +func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx).WithName("cluster-extension-revision") + ctx = log.IntoContext(ctx, l) + + existingRev := &ocv1.ClusterExtensionRevision{} + if err := c.Client.Get(ctx, req.NamespacedName, existingRev); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + l.Info("reconcile starting") + defer l.Info("reconcile ending") + + reconciledRev := existingRev.DeepCopy() + res, reconcileErr := c.reconcile(ctx, reconciledRev) + + // Do checks before any Update()s, as Update() may modify the resource structure! + updateStatus := !equality.Semantic.DeepEqual(existingRev.Status, reconciledRev.Status) + + unexpectedFieldsChanged := checkForUnexpectedClusterExtensionRevisionFieldChange(*existingRev, *reconciledRev) + if unexpectedFieldsChanged { + panic("spec or metadata changed by reconciler") + } + + // NOTE: finalizer updates are performed during c.reconcile as patches, so that reconcile can + // continue performing logic after successfully setting the finalizer. therefore we only need + // to set status here. + + if updateStatus { + if err := c.Client.Status().Update(ctx, reconciledRev); err != nil { + reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating status: %v", err)) + } + } + + return res, reconcileErr +} + +// Compare resources - ignoring status & metadata.finalizers +func checkForUnexpectedClusterExtensionRevisionFieldChange(a, b ocv1.ClusterExtensionRevision) bool { + a.Status, b.Status = ocv1.ClusterExtensionRevisionStatus{}, ocv1.ClusterExtensionRevisionStatus{} + + // when finalizers are updated during reconcile, we expect finalizers, managedFields, and resourceVersion + // to be updated, so we ignore changes in these fields. + a.Finalizers, b.Finalizers = []string{}, []string{} + a.ManagedFields, b.ManagedFields = nil, nil + a.ResourceVersion, b.ResourceVersion = "", "" + return !equality.Semantic.DeepEqual(a.Spec, b.Spec) +} + +func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (ctrl.Result, error) { + l := log.FromContext(ctx) + + revision, opts, previous := toBoxcutterRevision(rev) + + if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { + return c.teardown(ctx, rev, revision) + } + + // + // Reconcile + // + if err := c.ensureFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("error ensuring teardown finalizer: %v", err) + } + + if err := c.establishWatch(ctx, rev, revision); err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("establish watch: %v", err) + } + + rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...) + if err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("revision reconcile: %v", err) + } + l.Info("reconcile report", "report", rres.String()) + + // Retry failing preflight checks with a flat 10s retry. + // TODO: report status, backoff? + if verr := rres.GetValidationError(); verr != nil { + l.Info("preflight error, retrying after 10s", "err", verr.String()) + + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonRevisionValidationFailure, + Message: fmt.Sprintf("revision validation error: %s", verr), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + for i, pres := range rres.GetPhases() { + if verr := pres.GetValidationError(); verr != nil { + l.Info("preflight error, retrying after 10s", "err", verr.String()) + + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonPhaseValidationError, + Message: fmt.Sprintf("phase %d validation error: %s", i, verr), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + var collidingObjs []string + for _, ores := range pres.GetObjects() { + if ores.Action() == machinery.ActionCollision { + collidingObjs = append(collidingObjs, ores.String()) + } + } + + if len(collidingObjs) > 0 { + l.Info("object collision error, retrying after 10s", "collisions", collidingObjs) + + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonObjectCollisions, + Message: fmt.Sprintf("revision object collisions in phase %d\n%s", i, strings.Join(collidingObjs, "\n\n")), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + } + + //nolint:nestif + if rres.IsComplete() { + // Archive other revisions. + for _, a := range previous { + patch := []byte(`{"spec":{"lifecycleState":"Archived"}}`) + if err := c.Client.Patch(ctx, a, client.RawPatch(types.MergePatchType, patch)); err != nil { + // TODO: It feels like an error here needs to propagate to a status _somewhere_. + // Not sure the current CER makes sense? But it also feels off to set the CE + // status from outside the CE reconciler. + return ctrl.Result{}, fmt.Errorf("archive previous Revision: %w", err) + } + } + + // Report status. + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonAvailable, + Message: "Object is available and passes all probes.", + ObservedGeneration: rev.Generation, + }) + if !meta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeSucceeded, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRolloutSuccess, + Message: "Revision succeeded rolling out.", + ObservedGeneration: rev.Generation, + }) + } + } else { + var probeFailureMsgs []string + for _, pres := range rres.GetPhases() { + if pres.IsComplete() { + continue + } + for _, ores := range pres.GetObjects() { + pr := ores.Probes()[boxcutter.ProgressProbeType] + if pr.Success { + continue + } + + obj := ores.Object() + gvk := obj.GetObjectKind().GroupVersionKind() + probeFailureMsgs = append(probeFailureMsgs, fmt.Sprintf( + "Object %s.%s %s/%s: %v", + gvk.Kind, gvk.GroupVersion().String(), + obj.GetNamespace(), obj.GetName(), strings.Join(pr.Messages, " and "), + )) + break + } + } + if len(probeFailureMsgs) > 0 { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonProbeFailure, + Message: strings.Join(probeFailureMsgs, "\n"), + ObservedGeneration: rev.Generation, + }) + } else { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonIncomplete, + Message: "Revision has not been rolled out completely.", + ObservedGeneration: rev.Generation, + }) + } + } + if rres.InTransistion() { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonProgressing, + Message: "Rollout in progress.", + ObservedGeneration: rev.Generation, + }) + } else { + meta.RemoveStatusCondition(&rev.Status.Conditions, ocv1.TypeProgressing) + } + + return ctrl.Result{}, nil +} + +func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *ocv1.ClusterExtensionRevision, revision *boxcutter.Revision) (ctrl.Result, error) { + l := log.FromContext(ctx) + + tres, err := c.RevisionEngine.Teardown(ctx, *revision) + if err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err) + } + + l.Info("teardown report", "report", tres.String()) + if !tres.IsComplete() { + // TODO: If it is not complete, it seems like it would be good to update + // the status in some way to tell the user that the teardown is still + // in progress. + return ctrl.Result{}, nil + } + + if err := c.TrackingCache.Free(ctx, rev); err != nil { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionFalse, + Reason: ocv1.ClusterExtensionRevisionReasonReconcileFailure, + Message: err.Error(), + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err) + } + + // Ensure Available condition is set to Unknown before removing the finalizer when archiving + if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived && + !meta.IsStatusConditionPresentAndEqual(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable, metav1.ConditionUnknown) { + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionUnknown, + Reason: ocv1.ClusterExtensionRevisionReasonArchived, + Message: "revision is archived", + ObservedGeneration: rev.Generation, + }) + return ctrl.Result{}, nil + } + + if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err) + } + return ctrl.Result{}, nil +} + +type Sourcerer interface { + Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source +} + +func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For( + &ocv1.ClusterExtensionRevision{}, + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + WatchesRawSource( + c.TrackingCache.Source( + handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &ocv1.ClusterExtensionRevision{}), + predicate.ResourceVersionChangedPredicate{}, + ), + ). + Complete(c) +} + +func (c *ClusterExtensionRevisionReconciler) establishWatch( + ctx context.Context, rev *ocv1.ClusterExtensionRevision, + boxcutterRev *boxcutter.Revision, +) error { + gvks := sets.New[schema.GroupVersionKind]() + for _, phase := range boxcutterRev.Phases { + for _, obj := range phase.Objects { + gvks.Insert(obj.GroupVersionKind()) + } + } + + return c.TrackingCache.Watch(ctx, rev, gvks) +} + +func (c *ClusterExtensionRevisionReconciler) ensureFinalizer( + ctx context.Context, obj client.Object, finalizer string, +) error { + if controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + controllerutil.AddFinalizer(obj, finalizer) + patch := map[string]any{ + "metadata": map[string]any{ + "resourceVersion": obj.GetResourceVersion(), + "finalizers": obj.GetFinalizers(), + }, + } + patchJSON, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("marshalling patch to remove finalizer: %w", err) + } + if err := c.Client.Patch(ctx, obj, client.RawPatch(types.MergePatchType, patchJSON)); err != nil { + return fmt.Errorf("adding finalizer: %w", err) + } + return nil +} + +func (c *ClusterExtensionRevisionReconciler) removeFinalizer(ctx context.Context, obj client.Object, finalizer string) error { + if !controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + controllerutil.RemoveFinalizer(obj, finalizer) + + patch := map[string]any{ + "metadata": map[string]any{ + "resourceVersion": obj.GetResourceVersion(), + "finalizers": obj.GetFinalizers(), + }, + } + patchJSON, err := json.Marshal(patch) + if err != nil { + return fmt.Errorf("marshalling patch to remove finalizer: %w", err) + } + if err := c.Client.Patch(ctx, obj, client.RawPatch(types.MergePatchType, patchJSON)); err != nil { + return fmt.Errorf("removing finalizer: %w", err) + } + return nil +} + +func toBoxcutterRevision(rev *ocv1.ClusterExtensionRevision) (*boxcutter.Revision, []boxcutter.RevisionReconcileOption, []client.Object) { + previous := make([]client.Object, 0, len(rev.Spec.Previous)) + for _, specPrevious := range rev.Spec.Previous { + prev := &unstructured.Unstructured{} + prev.SetName(specPrevious.Name) + prev.SetUID(specPrevious.UID) + prev.SetGroupVersionKind(ocv1.GroupVersion.WithKind(ocv1.ClusterExtensionRevisionKind)) + previous = append(previous, prev) + } + + opts := []boxcutter.RevisionReconcileOption{ + boxcutter.WithPreviousOwners(previous), + boxcutter.WithProbe(boxcutter.ProgressProbeType, probing.And{ + deploymentProbe, statefulSetProbe, crdProbe, issuerProbe, certProbe, + }), + } + + r := &boxcutter.Revision{ + Name: rev.Name, + Owner: rev, + Revision: rev.Spec.Revision, + } + for _, specPhase := range rev.Spec.Phases { + phase := boxcutter.Phase{Name: specPhase.Name} + for _, specObj := range specPhase.Objects { + obj := specObj.Object.DeepCopy() + + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[ClusterExtensionRevisionOwnerLabel] = rev.Labels[ClusterExtensionRevisionOwnerLabel] + obj.SetLabels(labels) + + switch specObj.CollisionProtection { + case ocv1.CollisionProtectionIfNoController, ocv1.CollisionProtectionNone: + opts = append(opts, boxcutter.WithObjectReconcileOptions( + obj, boxcutter.WithCollisionProtection(specObj.CollisionProtection))) + } + + phase.Objects = append(phase.Objects, *obj) + } + r.Phases = append(r.Phases, phase) + } + + if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStatePaused { + opts = append(opts, boxcutter.WithPaused{}) + } + return r, opts, previous +} + +var ( + deploymentProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: appsv1.GroupName, Kind: "Deployment"}, + Prober: deplStatefulSetProbe, + } + statefulSetProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: appsv1.GroupName, Kind: "StatefulSet"}, + Prober: deplStatefulSetProbe, + } + crdProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}, + Prober: &probing.ObservedGenerationProbe{ + Prober: &probing.ConditionProbe{ // "Available" == "True" + Type: string(apiextensions.Established), + Status: string(corev1.ConditionTrue), + }, + }, + } + certProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: "acme.cert-manager.io", Kind: "Certificate"}, + Prober: &probing.ObservedGenerationProbe{ + Prober: readyConditionProbe, + }, + } + issuerProbe = &probing.GroupKindSelector{ + GroupKind: schema.GroupKind{Group: "acme.cert-manager.io", Kind: "Issuer"}, + Prober: &probing.ObservedGenerationProbe{ + Prober: readyConditionProbe, + }, + } + + // deplStaefulSetProbe probes Deployment, StatefulSet objects. + deplStatefulSetProbe = &probing.ObservedGenerationProbe{ + Prober: probing.And{ + availableConditionProbe, + replicasUpdatedProbe, + }, + } + + // Checks if the Type: "Available" Condition is "True". + availableConditionProbe = &probing.ConditionProbe{ // "Available" == "True" + Type: string(appsv1.DeploymentAvailable), + Status: string(corev1.ConditionTrue), + } + + // Checks if the Type: "Ready" Condition is "True" + readyConditionProbe = &probing.ObservedGenerationProbe{ + Prober: &probing.ConditionProbe{ + Type: "Ready", + Status: "True", + }, + } + + // Checks if .status.updatedReplicas == .status.replicas. + // Works for StatefulSts, Deployments and ReplicaSets. + replicasUpdatedProbe = &probing.FieldsEqualProbe{ + FieldA: ".status.updatedReplicas", + FieldB: ".status.replicas", + } +) diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go new file mode 100644 index 0000000000..873a6cc748 --- /dev/null +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go @@ -0,0 +1,902 @@ +package controllers_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "pkg.package-operator.run/boxcutter" + "pkg.package-operator.run/boxcutter/machinery" + machinerytypes "pkg.package-operator.run/boxcutter/machinery/types" + "pkg.package-operator.run/boxcutter/validation" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/source" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" +) + +func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionProgression(t *testing.T) { + const ( + clusterExtensionRevisionName = "test-ext-1" + ) + + testScheme := newScheme(t) + + for _, tc := range []struct { + name string + existingObjs func() []client.Object + revisionResult machinery.RevisionResult + validate func(*testing.T, client.Client) + }{ + { + name: "sets teardown finalizer", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown") + }, + }, + { + name: "set Available:False:InComplete status condition during rollout when no probe failures are detected", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionFalse, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonIncomplete, cond.Reason) + require.Equal(t, "Revision has not been rolled out completely.", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, + { + name: "set Available:False:ProbeFailure condition when probe failures are detected", + revisionResult: mockRevisionResult{ + phases: []machinery.PhaseResult{ + mockPhaseResult{ + name: "somephase", + isComplete: false, + objects: []machinery.ObjectResult{ + mockObjectResult{ + success: true, + probes: map[string]machinery.ObjectProbeResult{ + boxcutter.ProgressProbeType: { + Success: true, + }, + }, + }, + mockObjectResult{ + success: false, + object: func() client.Object { + obj := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-service", + Namespace: "my-namespace", + }, + } + obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) + return obj + }(), + probes: map[string]machinery.ObjectProbeResult{ + boxcutter.ProgressProbeType: { + Success: false, + Messages: []string{ + "something bad happened", + "something worse happened", + }, + }, + }, + }, + }, + }, + mockPhaseResult{ + name: "someotherphase", + isComplete: false, + objects: []machinery.ObjectResult{ + mockObjectResult{ + success: false, + object: func() client.Object { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-configmap", + Namespace: "my-namespace", + }, + } + obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) + return obj + }(), + probes: map[string]machinery.ObjectProbeResult{ + boxcutter.ProgressProbeType: { + Success: false, + Messages: []string{ + "we have a problem", + }, + }, + }, + }, + }, + }, + }, + }, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionFalse, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason) + require.Equal(t, "Object Service.v1 my-namespace/my-service: something bad happened and something worse happened\nObject ConfigMap.v1 my-namespace/my-configmap: we have a problem", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, + { + name: "set Progressing:True:Progressing condition while revision is transitioning", + revisionResult: mockRevisionResult{ + inTransition: true, + }, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonProgressing, cond.Reason) + require.Equal(t, "Rollout in progress.", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, + { + name: "remove Progressing condition once transition rollout is finished", + revisionResult: mockRevisionResult{ + inTransition: false, + }, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonProgressing, + Message: "some message", + ObservedGeneration: 1, + }) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.TypeProgressing) + require.Nil(t, cond) + }, + }, + { + name: "set Available:True:Available and Succeeded:True:RolloutSuccess conditions on successful revision rollout", + revisionResult: mockRevisionResult{ + isComplete: true, + }, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonAvailable, cond.Reason) + require.Equal(t, "Object is available and passes all probes.", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + + cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonRolloutSuccess, cond.Reason) + require.Equal(t, "Revision succeeded rolling out.", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, + { + name: "archive previous revisions on successful rollout", + revisionResult: mockRevisionResult{ + isComplete: true, + }, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + prevRev1 := newTestClusterExtensionRevision("prev-rev-1") + require.NoError(t, controllerutil.SetControllerReference(ext, prevRev1, testScheme)) + prevRev2 := newTestClusterExtensionRevision("prev-rev-2") + require.NoError(t, controllerutil.SetControllerReference(ext, prevRev2, testScheme)) + rev1 := newTestClusterExtensionRevision("test-ext-1") + rev1.Spec.Previous = []ocv1.ClusterExtensionRevisionPrevious{ + { + Name: "prev-rev-1", + UID: "prev-rev-1", + }, { + Name: "prev-rev-2", + UID: "prev-rev-2", + }, + } + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{ext, prevRev1, prevRev2, rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: "prev-rev-1", + }, rev) + require.NoError(t, err) + require.Equal(t, ocv1.ClusterExtensionRevisionLifecycleStateArchived, rev.Spec.LifecycleState) + + err = c.Get(t.Context(), client.ObjectKey{ + Name: "prev-rev-2", + }, rev) + require.NoError(t, err) + require.Equal(t, ocv1.ClusterExtensionRevisionLifecycleStateArchived, rev.Spec.LifecycleState) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + // create extension and cluster extension + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithStatusSubresource(&ocv1.ClusterExtensionRevision{}). + WithObjects(tc.existingObjs()...). + Build() + + // reconcile cluster extension revision + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngine: &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil + }, + }, + TrackingCache: &mockTrackingCache{}, + }).Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: clusterExtensionRevisionName, + }, + }) + + // reconcile cluster extensionr evision + require.Equal(t, ctrl.Result{}, result) + require.NoError(t, err) + + // validate test case + tc.validate(t, testClient) + }) + } +} + +func Test_ClusterExtensionRevisionReconciler_Reconcile_ValidationError_Retries(t *testing.T) { + const ( + clusterExtensionName = "test-ext" + clusterExtensionRevisionName = "test-ext-1" + ) + + testScheme := newScheme(t) + + for _, tc := range []struct { + name string + revisionResult machinery.RevisionResult + }{ + { + name: "retries on revision result validation error", + revisionResult: mockRevisionResult{ + validationError: &validation.RevisionValidationError{ + RevisionName: "test-ext-1", + RevisionNumber: 1, + Phases: []validation.PhaseValidationError{ + { + PhaseName: "everything", + PhaseError: fmt.Errorf("some error"), + Objects: []validation.ObjectValidationError{ + { + ObjectRef: machinerytypes.ObjectRef{ + GroupVersionKind: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + }, + ObjectKey: client.ObjectKey{ + Name: "my-configmap", + Namespace: "my-namespace", + }, + }, + Errors: []error{ + fmt.Errorf("is not a config"), + fmt.Errorf("is not a map"), + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "retries on revision result phase validation error", + revisionResult: mockRevisionResult{ + phases: []machinery.PhaseResult{ + mockPhaseResult{ + validationError: &validation.PhaseValidationError{ + PhaseName: "everything", + PhaseError: fmt.Errorf("some error"), + Objects: []validation.ObjectValidationError{ + { + ObjectRef: machinerytypes.ObjectRef{ + GroupVersionKind: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + }, + ObjectKey: client.ObjectKey{ + Name: "my-configmap", + Namespace: "my-namespace", + }, + }, + Errors: []error{ + fmt.Errorf("is not a config"), + fmt.Errorf("is not a map"), + }, + }, + }, + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + + // create extension and cluster extension + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithStatusSubresource(&ocv1.ClusterExtensionRevision{}). + WithObjects(ext, rev1). + Build() + + // reconcile cluster extension revision + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngine: &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil + }, + }, + TrackingCache: &mockTrackingCache{}, + }).Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: clusterExtensionRevisionName, + }, + }) + + // reconcile cluster extensionr evision + require.Equal(t, ctrl.Result{ + RequeueAfter: 10 * time.Second, + }, result) + require.NoError(t, err) + }) + } +} + +func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { + const ( + clusterExtensionRevisionName = "test-ext-1" + ) + + testScheme := newScheme(t) + require.NoError(t, corev1.AddToScheme(testScheme)) + + for _, tc := range []struct { + name string + existingObjs func() []client.Object + revisionResult machinery.RevisionResult + revisionEngineTeardownFn func(*testing.T) func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) + validate func(*testing.T, client.Client) + expectedErr string + }{ + { + name: "teardown finalizer is removed", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + return []client.Object{rev1} + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) + }, + revisionEngineTeardownFn: func(t *testing.T) func(context.Context, machinerytypes.Revision, ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return nil + }, + }, + { + name: "revision is torn down and deleted when deleted", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()} + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return &mockRevisionTeardownResult{ + isComplete: true, + }, nil + } + }, + validate: func(t *testing.T, c client.Client) { + t.Log("cluster revision is deleted") + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.Error(t, err) + require.True(t, errors.IsNotFound(err)) + }, + }, + { + name: "surfaces tear down errors when deleted", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()} + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return nil, fmt.Errorf("some teardown error") + } + }, + expectedErr: "some teardown error", + validate: func(t *testing.T, c client.Client) { + t.Log("cluster revision is not deleted and still contains finalizer") + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) + }, + }, + { + name: "set Available condition to Unknown with reason Archived when archiving revision", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return &mockRevisionTeardownResult{ + isComplete: true, + }, nil + } + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionUnknown, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) + require.Equal(t, "revision is archived", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + }, + }, + { + name: "revision is torn down when in archived state and finalizer is removed", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeAvailable, + Status: metav1.ConditionUnknown, + Reason: ocv1.ClusterExtensionRevisionReasonArchived, + Message: "revision is archived", + ObservedGeneration: rev1.Generation, + }) + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return &mockRevisionTeardownResult{ + isComplete: true, + }, nil + } + }, + validate: func(t *testing.T, c client.Client) { + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + require.NotContains(t, rev.Finalizers, "olm.operatorframework.io/teardown") + }, + }, + { + name: "surfaces revision teardown error when in archived state", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(clusterExtensionRevisionName) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + require.NoError(t, controllerutil.SetControllerReference(ext, rev1, testScheme)) + return []client.Object{rev1, ext} + }, + revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return nil, fmt.Errorf("some teardown error") + } + }, + expectedErr: "some teardown error", + validate: func(t *testing.T, c client.Client) { + t.Log("cluster revision is not deleted and still contains finalizer") + rev := &ocv1.ClusterExtensionRevision{} + err := c.Get(t.Context(), client.ObjectKey{ + Name: clusterExtensionRevisionName, + }, rev) + require.NoError(t, err) + require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + // create extension and cluster extension + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithStatusSubresource(&ocv1.ClusterExtensionRevision{}). + WithObjects(tc.existingObjs()...). + Build() + + // reconcile cluster extension revision + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngine: &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil + }, + teardown: tc.revisionEngineTeardownFn(t), + }, + TrackingCache: &mockTrackingCache{}, + }).Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: clusterExtensionRevisionName, + }, + }) + + // reconcile cluster extension revision + require.Equal(t, ctrl.Result{}, result) + if tc.expectedErr != "" { + require.Contains(t, err.Error(), tc.expectedErr) + } else { + require.NoError(t, err) + } + + // validate test case + tc.validate(t, testClient) + }) + } +} + +func newTestClusterExtension() *ocv1.ClusterExtension { + return &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + UID: "test-ext", + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "some-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "service-account", + }, + Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeCatalog, + Catalog: &ocv1.CatalogFilter{ + PackageName: "some-package", + }, + }, + }, + } +} + +func newTestClusterExtensionRevision(name string) *ocv1.ClusterExtensionRevision { + return &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + UID: types.UID(name), + Generation: int64(1), + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: "everything", + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +type mockRevisionEngine struct { + teardown func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) + reconcile func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) +} + +func (m mockRevisionEngine) Teardown(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { + return m.teardown(ctx, rev) +} + +func (m mockRevisionEngine) Reconcile(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return m.reconcile(ctx, rev) +} + +type mockRevisionResult struct { + validationError *validation.RevisionValidationError + phases []machinery.PhaseResult + inTransition bool + isComplete bool + hasProgressed bool + string string +} + +func (m mockRevisionResult) GetValidationError() *validation.RevisionValidationError { + return m.validationError +} + +func (m mockRevisionResult) GetPhases() []machinery.PhaseResult { + return m.phases +} + +func (m mockRevisionResult) InTransistion() bool { + return m.inTransition +} + +func (m mockRevisionResult) IsComplete() bool { + return m.isComplete +} + +func (m mockRevisionResult) HasProgressed() bool { + return m.hasProgressed +} + +func (m mockRevisionResult) String() string { + return m.string +} + +type mockPhaseResult struct { + name string + validationError *validation.PhaseValidationError + objects []machinery.ObjectResult + inTransition bool + isComplete bool + hasProgressed bool + string string +} + +func (m mockPhaseResult) GetName() string { + return m.name +} + +func (m mockPhaseResult) GetValidationError() *validation.PhaseValidationError { + return m.validationError +} + +func (m mockPhaseResult) GetObjects() []machinery.ObjectResult { + return m.objects +} + +func (m mockPhaseResult) InTransistion() bool { + return m.inTransition +} + +func (m mockPhaseResult) IsComplete() bool { + return m.isComplete +} + +func (m mockPhaseResult) HasProgressed() bool { + return m.hasProgressed +} + +func (m mockPhaseResult) String() string { + return m.string +} + +type mockObjectResult struct { + action machinery.Action + object machinery.Object + success bool + probes map[string]machinery.ObjectProbeResult + string string +} + +func (m mockObjectResult) Action() machinery.Action { + return m.action +} + +func (m mockObjectResult) Object() machinery.Object { + return m.object +} + +func (m mockObjectResult) Success() bool { + return m.success +} + +func (m mockObjectResult) Probes() map[string]machinery.ObjectProbeResult { + return m.probes +} + +func (m mockObjectResult) String() string { + return m.string +} + +type mockRevisionTeardownResult struct { + phases []machinery.PhaseTeardownResult + isComplete bool + waitingPhaseNames []string + activePhaseName string + phaseIsActive bool + gonePhaseNames []string + string string +} + +func (m mockRevisionTeardownResult) GetPhases() []machinery.PhaseTeardownResult { + return m.phases +} + +func (m mockRevisionTeardownResult) IsComplete() bool { + return m.isComplete +} + +func (m mockRevisionTeardownResult) GetWaitingPhaseNames() []string { + return m.waitingPhaseNames +} + +func (m mockRevisionTeardownResult) GetActivePhaseName() (string, bool) { + return m.activePhaseName, m.phaseIsActive +} + +func (m mockRevisionTeardownResult) GetGonePhaseNames() []string { + return m.gonePhaseNames +} + +func (m mockRevisionTeardownResult) String() string { + return m.string +} + +type mockTrackingCache struct{} + +func (m *mockTrackingCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + panic("not implemented") +} + +func (m *mockTrackingCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + panic("not implemented") +} + +func (m *mockTrackingCache) Source(handler handler.EventHandler, predicates ...predicate.Predicate) source.Source { + panic("not implemented") +} + +func (m *mockTrackingCache) Watch(ctx context.Context, user client.Object, gvks sets.Set[schema.GroupVersionKind]) error { + return nil +} + +func (m *mockTrackingCache) Free(ctx context.Context, user client.Object) error { + return nil +} diff --git a/internal/operator-controller/controllers/common_controller.go b/internal/operator-controller/controllers/common_controller.go index 7cee10c100..7fafc7bb7b 100644 --- a/internal/operator-controller/controllers/common_controller.go +++ b/internal/operator-controller/controllers/common_controller.go @@ -27,25 +27,53 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" ) -// setInstalledStatusFromBundle sets the installed status based on the given installedBundle. -func setInstalledStatusFromBundle(ext *ocv1.ClusterExtension, installedBundle *InstalledBundle) { +const ( + // maxConditionMessageLength set the max message length allowed by Kubernetes. + maxConditionMessageLength = 32768 + // truncationSuffix is the suffix added when a message is cut. + truncationSuffix = "\n\n... [message truncated]" +) + +// truncateMessage cuts long messages to fit Kubernetes condition limits +func truncateMessage(message string) string { + if len(message) <= maxConditionMessageLength { + return message + } + + maxContent := maxConditionMessageLength - len(truncationSuffix) + return message[:maxContent] + truncationSuffix +} + +// SetStatusCondition wraps apimeta.SetStatusCondition and ensures the message is always truncated +// This should be used throughout the codebase instead of apimeta.SetStatusCondition directly +func SetStatusCondition(conditions *[]metav1.Condition, condition metav1.Condition) { + condition.Message = truncateMessage(condition.Message) + apimeta.SetStatusCondition(conditions, condition) +} + +// setInstalledStatusFromRevisionStates sets the installed status based on the given installedBundle. +func setInstalledStatusFromRevisionStates(ext *ocv1.ClusterExtension, revisionStates *RevisionStates) { // Nothing is installed - if installedBundle == nil { + if revisionStates.Installed == nil { setInstallStatus(ext, nil) - setInstalledStatusConditionFailed(ext, "No bundle installed") + if len(revisionStates.RollingOut) == 0 { + setInstalledStatusConditionFalse(ext, ocv1.ReasonFailed, "No bundle installed") + } else { + setInstalledStatusConditionFalse(ext, ocv1.ReasonAbsent, "No bundle installed") + } return } // Something is installed installStatus := &ocv1.ClusterExtensionInstallStatus{ - Bundle: installedBundle.BundleMetadata, + Bundle: revisionStates.Installed.BundleMetadata, } setInstallStatus(ext, installStatus) - setInstalledStatusConditionSuccess(ext, fmt.Sprintf("Installed bundle %s successfully", installedBundle.Image)) + setInstalledStatusConditionSuccess(ext, fmt.Sprintf("Installed bundle %s successfully", revisionStates.Installed.Image)) } // setInstalledStatusConditionSuccess sets the installed status condition to success. func setInstalledStatusConditionSuccess(ext *ocv1.ClusterExtension, message string) { - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeInstalled, Status: metav1.ConditionTrue, Reason: ocv1.ReasonSucceeded, @@ -55,11 +83,11 @@ func setInstalledStatusConditionSuccess(ext *ocv1.ClusterExtension, message stri } // setInstalledStatusConditionFailed sets the installed status condition to failed. -func setInstalledStatusConditionFailed(ext *ocv1.ClusterExtension, message string) { - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ +func setInstalledStatusConditionFalse(ext *ocv1.ClusterExtension, reason string, message string) { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeInstalled, Status: metav1.ConditionFalse, - Reason: ocv1.ReasonFailed, + Reason: reason, Message: message, ObservedGeneration: ext.GetGeneration(), }) @@ -67,7 +95,7 @@ func setInstalledStatusConditionFailed(ext *ocv1.ClusterExtension, message strin // setInstalledStatusConditionUnknown sets the installed status condition to unknown. func setInstalledStatusConditionUnknown(ext *ocv1.ClusterExtension, message string) { - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: ocv1.TypeInstalled, Status: metav1.ConditionUnknown, Reason: ocv1.ReasonFailed, @@ -85,7 +113,7 @@ func setStatusProgressing(ext *ocv1.ClusterExtension, err error) { Type: ocv1.TypeProgressing, Status: metav1.ConditionTrue, Reason: ocv1.ReasonSucceeded, - Message: "desired state reached", + Message: "Desired state reached", ObservedGeneration: ext.GetGeneration(), } @@ -99,5 +127,5 @@ func setStatusProgressing(ext *ocv1.ClusterExtension, err error) { progressingCond.Reason = ocv1.ReasonBlocked } - apimeta.SetStatusCondition(&ext.Status.Conditions, progressingCond) + SetStatusCondition(&ext.Status.Conditions, progressingCond) } diff --git a/internal/operator-controller/controllers/common_controller_test.go b/internal/operator-controller/controllers/common_controller_test.go index 7b644172d1..4d0a0536d1 100644 --- a/internal/operator-controller/controllers/common_controller_test.go +++ b/internal/operator-controller/controllers/common_controller_test.go @@ -2,6 +2,8 @@ package controllers import ( "errors" + "fmt" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -29,7 +31,7 @@ func TestSetStatusProgressing(t *testing.T) { Type: ocv1.TypeProgressing, Status: metav1.ConditionTrue, Reason: ocv1.ReasonSucceeded, - Message: "desired state reached", + Message: "Desired state reached", }, }, { @@ -64,3 +66,177 @@ func TestSetStatusProgressing(t *testing.T) { }) } } + +func TestTruncateMessage(t *testing.T) { + tests := []struct { + name string + message string + expected string + }{ + { + name: "short message unchanged", + message: "This is a short message", + expected: "This is a short message", + }, + { + name: "empty message unchanged", + message: "", + expected: "", + }, + { + name: "exact max length message unchanged", + message: strings.Repeat("a", maxConditionMessageLength), + expected: strings.Repeat("a", maxConditionMessageLength), + }, + { + name: "message just over limit gets truncated", + message: strings.Repeat("a", maxConditionMessageLength+1), + expected: strings.Repeat("a", maxConditionMessageLength-len(truncationSuffix)) + truncationSuffix, + }, + { + name: "very long message gets truncated", + message: strings.Repeat("word ", 10000) + "finalword", + expected: strings.Repeat("word ", 10000)[:maxConditionMessageLength-len(truncationSuffix)] + truncationSuffix, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := truncateMessage(tc.message) + require.Equal(t, tc.expected, result) + + // Verify the result is within the limit + require.LessOrEqual(t, len(result), maxConditionMessageLength, + "truncated message should not exceed max length") + + // If the original message was over the limit, verify truncation occurred + if len(tc.message) > maxConditionMessageLength { + require.Contains(t, result, truncationSuffix, + "long messages should contain truncation suffix") + require.Less(t, len(result), len(tc.message), + "truncated message should be shorter than original") + } + }) + } +} + +func TestSetStatusProgressingWithLongMessage(t *testing.T) { + // Simulate a real ClusterExtension CRD upgrade safety check failure with many validation errors + longError := fmt.Sprintf("validating CRD upgrade safety for ClusterExtension 'my-operator': %s", + strings.Repeat("CRD \"myresources.example.com\" v1beta1->v1: field .spec.replicas changed from optional to required, field .spec.config.timeout type changed from string to integer, field .status.conditions[].observedGeneration removed\n", 500)) + + ext := &ocv1.ClusterExtension{ObjectMeta: metav1.ObjectMeta{Name: "my-operator"}} + err := errors.New(longError) + setStatusProgressing(ext, err) + + cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, cond) + require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength) + require.Contains(t, cond.Message, truncationSuffix) + require.Contains(t, cond.Message, "validating CRD upgrade safety") +} + +func TestClusterExtensionDeprecationMessageTruncation(t *testing.T) { + // Test truncation for ClusterExtension deprecation warnings with many deprecated APIs + ext := &ocv1.ClusterExtension{ObjectMeta: metav1.ObjectMeta{Name: "legacy-operator"}} + + // Simulate many deprecation warnings that would overflow the message limit + deprecationMessages := []string{} + for i := 0; i < 1000; i++ { + deprecationMessages = append(deprecationMessages, fmt.Sprintf("API version 'v1beta1' of resource 'customresources%d.example.com' is deprecated, use 'v1' instead", i)) + } + + longDeprecationMsg := strings.Join(deprecationMessages, "; ") + setInstalledStatusConditionUnknown(ext, longDeprecationMsg) + + cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, cond) + require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength) + require.Contains(t, cond.Message, truncationSuffix, "deprecation messages should be truncated when too long") + require.Contains(t, cond.Message, "API version", "should preserve important deprecation context") +} + +func TestClusterExtensionInstallationFailureTruncation(t *testing.T) { + // Test truncation for ClusterExtension installation failures with many bundle validation errors + installError := "failed to install ClusterExtension 'argocd-operator': bundle validation errors: " + + strings.Repeat("resource 'deployments/argocd-server' missing required label 'app.kubernetes.io/name', resource 'services/argocd-server-metrics' has invalid port configuration, resource 'configmaps/argocd-cm' contains invalid YAML in data field 'application.yaml'\n", 400) + + ext := &ocv1.ClusterExtension{ObjectMeta: metav1.ObjectMeta{Name: "argocd-operator"}} + setInstalledStatusConditionFalse(ext, ocv1.ReasonFailed, installError) + + cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, cond) + + // Verify message was truncated due to length + require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength) + require.Contains(t, cond.Message, truncationSuffix, "installation failure messages should be truncated when too long") + require.Contains(t, cond.Message, "failed to install ClusterExtension", "should preserve important context") + require.Equal(t, metav1.ConditionFalse, cond.Status) + require.Equal(t, ocv1.ReasonFailed, cond.Reason) + + // Verify original message was actually longer than the limit + require.Greater(t, len(installError), maxConditionMessageLength, "test should use a message that exceeds the limit") +} + +func TestSetStatusConditionWrapper(t *testing.T) { + tests := []struct { + name string + message string + expectedTruncated bool + }{ + { + name: "short message not truncated", + message: "This is a short message", + expectedTruncated: false, + }, + { + name: "long message gets truncated", + message: strings.Repeat("This is a very long message. ", 2000), + expectedTruncated: true, + }, + { + name: "message at exact limit not truncated", + message: strings.Repeat("a", maxConditionMessageLength), + expectedTruncated: false, + }, + { + name: "message over limit gets truncated", + message: strings.Repeat("a", maxConditionMessageLength+1), + expectedTruncated: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var conditions []metav1.Condition + + // Use our wrapper function + SetStatusCondition(&conditions, metav1.Condition{ + Type: "TestCondition", + Status: metav1.ConditionTrue, + Reason: "Testing", + Message: tc.message, + }) + + require.Len(t, conditions, 1, "should have exactly one condition") + cond := conditions[0] + + // Verify message is within limits + require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength, + "condition message should not exceed max length") + + // Check if truncation occurred as expected + if tc.expectedTruncated { + require.Contains(t, cond.Message, truncationSuffix, + "long messages should contain truncation suffix") + require.Less(t, len(cond.Message), len(tc.message), + "truncated message should be shorter than original") + } else { + require.Equal(t, tc.message, cond.Message, + "short messages should remain unchanged") + require.NotContains(t, cond.Message, truncationSuffix, + "short messages should not contain truncation suffix") + } + }) + } +} diff --git a/internal/operator-controller/controllers/pull_secret_controller.go b/internal/operator-controller/controllers/pull_secret_controller.go deleted file mode 100644 index d73ccddb3b..0000000000 --- a/internal/operator-controller/controllers/pull_secret_controller.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "fmt" - "os" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -// PullSecretReconciler reconciles a specific Secret object -// that contains global pull secrets for pulling bundle images -type PullSecretReconciler struct { - client.Client - SecretKey types.NamespacedName - AuthFilePath string -} - -func (r *PullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - if req.Name != r.SecretKey.Name || req.Namespace != r.SecretKey.Namespace { - logger.Error(fmt.Errorf("received unexpected request for Secret %v/%v", req.Namespace, req.Name), "reconciliation error") - return ctrl.Result{}, nil - } - - secret := &corev1.Secret{} - err := r.Get(ctx, req.NamespacedName, secret) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("secret not found") - return r.deleteSecretFile(logger) - } - logger.Error(err, "failed to get Secret") - return ctrl.Result{}, err - } - - return r.writeSecretToFile(logger, secret) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { - _, err := ctrl.NewControllerManagedBy(mgr). - Named("controller-operator-pull-secret-controller"). - For(&corev1.Secret{}). - WithEventFilter(newSecretPredicate(r.SecretKey)). - Build(r) - - return err -} - -func newSecretPredicate(key types.NamespacedName) predicate.Predicate { - return predicate.NewPredicateFuncs(func(obj client.Object) bool { - return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace - }) -} - -// writeSecretToFile writes the secret data to the specified file -func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secret *corev1.Secret) (ctrl.Result, error) { - // image registry secrets are always stored with the key .dockerconfigjson - // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials - dockerConfigJSON, ok := secret.Data[".dockerconfigjson"] - if !ok { - logger.Error(fmt.Errorf("expected secret.Data key not found"), "expected secret Data to contain key .dockerconfigjson") - return ctrl.Result{}, nil - } - // expected format for auth.json - // https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md - err := os.WriteFile(r.AuthFilePath, dockerConfigJSON, 0600) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err) - } - logger.Info("saved global pull secret data locally") - return ctrl.Result{}, nil -} - -// deleteSecretFile deletes the auth file if the secret is deleted -func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) { - logger.Info("deleting local auth file", "file", r.AuthFilePath) - if err := os.Remove(r.AuthFilePath); err != nil { - if os.IsNotExist(err) { - logger.Info("auth file does not exist, nothing to delete") - return ctrl.Result{}, nil - } - return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err) - } - logger.Info("auth file deleted successfully") - return ctrl.Result{}, nil -} diff --git a/internal/operator-controller/controllers/pull_secret_controller_test.go b/internal/operator-controller/controllers/pull_secret_controller_test.go deleted file mode 100644 index 4406ffa2f6..0000000000 --- a/internal/operator-controller/controllers/pull_secret_controller_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package controllers_test - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" - "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" -) - -func TestSecretSyncerReconciler(t *testing.T) { - secretData := []byte(`{"auths":{"exampleRegistry": "exampledata"}}`) - authFileName := "test-auth.json" - for _, tt := range []struct { - name string - secret *corev1.Secret - addSecret bool - wantErr string - fileShouldExistBefore bool - fileShouldExistAfter bool - }{ - { - name: "secret exists, content gets saved to authFile", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: true, - fileShouldExistBefore: false, - fileShouldExistAfter: true, - }, - { - name: "secret does not exist, file exists previously, file should get deleted", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: false, - fileShouldExistBefore: true, - fileShouldExistAfter: false, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - tempAuthFile := filepath.Join(t.TempDir(), authFileName) - clientBuilder := fake.NewClientBuilder().WithScheme(scheme.Scheme) - if tt.addSecret { - clientBuilder = clientBuilder.WithObjects(tt.secret) - } - cl := clientBuilder.Build() - - secretKey := types.NamespacedName{Namespace: tt.secret.Namespace, Name: tt.secret.Name} - r := &controllers.PullSecretReconciler{ - Client: cl, - SecretKey: secretKey, - AuthFilePath: tempAuthFile, - } - if tt.fileShouldExistBefore { - err := os.WriteFile(tempAuthFile, secretData, 0600) - require.NoError(t, err) - } - res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: secretKey}) - if tt.wantErr == "" { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, tt.wantErr) - } - require.Equal(t, ctrl.Result{}, res) - - if tt.fileShouldExistAfter { - _, err := os.Stat(tempAuthFile) - require.NoError(t, err) - } else { - _, err := os.Stat(tempAuthFile) - require.True(t, os.IsNotExist(err)) - } - }) - } -} diff --git a/internal/operator-controller/controllers/suite_test.go b/internal/operator-controller/controllers/suite_test.go index a83f0439c4..02d5382371 100644 --- a/internal/operator-controller/controllers/suite_test.go +++ b/internal/operator-controller/controllers/suite_test.go @@ -21,142 +21,78 @@ import ( "io/fs" "log" "os" - "path/filepath" "testing" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/meta" apimachineryruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" - helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" - ocv1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager" - cmcache "github.com/operator-framework/operator-controller/internal/operator-controller/contentmanager/cache" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" + "github.com/operator-framework/operator-controller/test" ) +func newScheme(t *testing.T) *apimachineryruntime.Scheme { + sch := apimachineryruntime.NewScheme() + require.NoError(t, ocv1.AddToScheme(sch)) + return sch +} + func newClient(t *testing.T) client.Client { // TODO: this is a live client, which behaves differently than a cache client. // We may want to use a caching client instead to get closer to real behavior. - sch := apimachineryruntime.NewScheme() - require.NoError(t, ocv1.AddToScheme(sch)) - cl, err := client.New(config, client.Options{Scheme: sch}) + cl, err := client.New(config, client.Options{Scheme: newScheme(t)}) require.NoError(t, err) require.NotNil(t, cl) return cl } -type MockInstalledBundleGetter struct { - bundle *controllers.InstalledBundle - err error -} +var _ controllers.RevisionStatesGetter = (*MockRevisionStatesGetter)(nil) -func (m *MockInstalledBundleGetter) SetBundle(bundle *controllers.InstalledBundle) { - m.bundle = bundle +type MockRevisionStatesGetter struct { + *controllers.RevisionStates + Err error } -func (m *MockInstalledBundleGetter) GetInstalledBundle(ctx context.Context, ext *ocv1.ClusterExtension) (*controllers.InstalledBundle, error) { - return m.bundle, m.err +func (m *MockRevisionStatesGetter) GetRevisionStates(ctx context.Context, ext *ocv1.ClusterExtension) (*controllers.RevisionStates, error) { + if m.Err != nil { + return nil, m.Err + } + return m.RevisionStates, nil } var _ controllers.Applier = (*MockApplier)(nil) type MockApplier struct { - err error - objs []client.Object - state string -} - -func (m *MockApplier) Apply(_ context.Context, _ fs.FS, _ *ocv1.ClusterExtension, _ map[string]string, _ map[string]string) ([]client.Object, string, error) { - if m.err != nil { - return nil, m.state, m.err - } - - return m.objs, m.state, nil -} - -var _ contentmanager.Manager = (*MockManagedContentCacheManager)(nil) - -type MockManagedContentCacheManager struct { - err error - cache cmcache.Cache -} - -func (m *MockManagedContentCacheManager) Get(_ context.Context, _ *ocv1.ClusterExtension) (cmcache.Cache, error) { - if m.err != nil { - return nil, m.err - } - return m.cache, nil -} - -func (m *MockManagedContentCacheManager) Delete(_ *ocv1.ClusterExtension) error { - return m.err -} - -type MockManagedContentCache struct { - err error -} - -var _ cmcache.Cache = (*MockManagedContentCache)(nil) - -func (m *MockManagedContentCache) Close() error { - if m.err != nil { - return m.err - } - return nil + installCompleted bool + installStatus string + err error } -func (m *MockManagedContentCache) Watch(_ context.Context, _ cmcache.Watcher, _ ...client.Object) error { - if m.err != nil { - return m.err - } - return nil +func (m *MockApplier) Apply(_ context.Context, _ fs.FS, _ *ocv1.ClusterExtension, _ map[string]string, _ map[string]string) (bool, string, error) { + return m.installCompleted, m.installStatus, m.err } func newClientAndReconciler(t *testing.T) (client.Client, *controllers.ClusterExtensionReconciler) { cl := newClient(t) reconciler := &controllers.ClusterExtensionReconciler{ - Client: cl, - InstalledBundleGetter: &MockInstalledBundleGetter{}, - Finalizers: crfinalizer.NewFinalizers(), + Client: cl, + RevisionStatesGetter: &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{}, + }, + Finalizers: crfinalizer.NewFinalizers(), } return cl, reconciler } -var ( - config *rest.Config - helmClientGetter helmclient.ActionClientGetter -) +var config *rest.Config func TestMain(m *testing.M) { - testEnv := &envtest.Environment{ - CRDDirectoryPaths: []string{ - filepath.Join("..", "..", "..", "config", "base", "operator-controller", "crd", "bases"), - }, - ErrorIfCRDPathMissing: true, - } - - // ENVTEST-based tests require specific binaries. By default, these binaries are located - // in paths defined by controller-runtime. However, the `BinaryAssetsDirectory` needs - // to be explicitly set when running tests directly (e.g., debugging tests in an IDE) - // without using the Makefile targets. - // - // This is equivalent to configuring your IDE to export the `KUBEBUILDER_ASSETS` environment - // variable before each test execution. The following function simplifies this process - // by handling the configuration for you. - // - // To ensure the binaries are in the expected path without manual configuration, run: - // `make envtest-k8s-bins` - if getFirstFoundEnvTestBinaryDir() != "" { - testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() - } + testEnv := test.NewEnv() var err error config, err = testEnv.Start() @@ -165,25 +101,7 @@ func TestMain(m *testing.M) { log.Panic("expected cfg to not be nil") } - rm := meta.NewDefaultRESTMapper(nil) - cfgGetter, err := helmclient.NewActionConfigGetter(config, rm) - utilruntime.Must(err) - helmClientGetter, err = helmclient.NewActionClientGetter(cfgGetter) - utilruntime.Must(err) - code := m.Run() utilruntime.Must(testEnv.Stop()) os.Exit(code) } - -// getFirstFoundEnvTestBinaryDir finds and returns the first directory under the given path. -func getFirstFoundEnvTestBinaryDir() string { - basePath := filepath.Join("..", "..", "bin", "envtest-binaries", "k8s") - entries, _ := os.ReadDir(basePath) - for _, entry := range entries { - if entry.IsDir() { - return filepath.Join(basePath, entry.Name()) - } - } - return "" -} diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 1de30e25ba..87e827c66f 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -16,6 +16,8 @@ const ( SyntheticPermissions featuregate.Feature = "SyntheticPermissions" WebhookProviderCertManager featuregate.Feature = "WebhookProviderCertManager" WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA" + HelmChartSupport featuregate.Feature = "HelmChartSupport" + BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime" ) var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -31,8 +33,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature // registry+v1 cluster extensions with single or own namespaces modes // i.e. with a single watch namespace. SingleOwnNamespaceInstallSupport: { - Default: false, - PreRelease: featuregate.Alpha, + Default: true, + PreRelease: featuregate.GA, LockToDefault: false, }, @@ -49,8 +51,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature // mutating, and/or conversion webhooks with CertManager // as the certificate provider. WebhookProviderCertManager: { - Default: false, - PreRelease: featuregate.Alpha, + Default: true, + PreRelease: featuregate.GA, LockToDefault: false, }, @@ -59,6 +61,21 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature // mutating, and/or conversion webhooks with Openshift Service CA // as the certificate provider. WebhookProviderOpenshiftServiceCA: { + Default: true, + PreRelease: featuregate.GA, + LockToDefault: false, + }, + + // HelmChartSupport enables support for installing, + // updating and uninstalling Helm Charts via Cluster Extensions. + HelmChartSupport: { + Default: false, + PreRelease: featuregate.Alpha, + LockToDefault: false, + }, + + // BoxcutterRuntime configures OLM to use the Boxcutter runtime for extension lifecycling + BoxcutterRuntime: { Default: false, PreRelease: featuregate.Alpha, LockToDefault: false, diff --git a/internal/operator-controller/resolve/catalog_test.go b/internal/operator-controller/resolve/catalog_test.go index 00467253e8..21232bc4df 100644 --- a/internal/operator-controller/resolve/catalog_test.go +++ b/internal/operator-controller/resolve/catalog_test.go @@ -618,7 +618,7 @@ func (w staticCatalogWalker) WalkCatalogs(ctx context.Context, _ string, f Catal for _, opt := range opts { opt.ApplyToList(&options) } - if !options.LabelSelector.Matches(labels.Set(cat.ObjectMeta.Labels)) { + if !options.LabelSelector.Matches(labels.Set(cat.Labels)) { continue } fbc, catCfg, fbcErr := v() diff --git a/internal/operator-controller/rukpak/bundle/config.go b/internal/operator-controller/rukpak/bundle/config.go new file mode 100644 index 0000000000..7ec4bfdf03 --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/config.go @@ -0,0 +1,124 @@ +package bundle + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +type Config struct { + WatchNamespace *string `json:"watchNamespace"` +} + +// UnmarshalConfig returns a deserialized *bundle.Config based on bytes and validated +// against rv1 and the desired install namespaces. It will error if: +// - rv is nil +// - bytes is not a valid YAML/JSON object +// - bytes is a valid YAML/JSON object but does not follow the registry+v1 schema +// - if bytes is nil, a nil *bundle.Config is returned with no error +func UnmarshalConfig(bytes []byte, rv1 RegistryV1, installNamespace string) (*Config, error) { + if bytes == nil { + return nil, nil + } + + bundleConfig := &Config{} + if err := yaml.UnmarshalStrict(bytes, bundleConfig); err != nil { + return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshalError(err)) + } + + // collect bundle install modes + bundleInstallModeSet := sets.New(rv1.CSV.Spec.InstallModes...) + + if err := validateConfig(bundleConfig, installNamespace, bundleInstallModeSet); err != nil { + return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", err) + } + + return bundleConfig, nil +} + +// validateConfig validates a *bundle.Config against the bundle's supported install modes and the user-give installNamespace. +func validateConfig(config *Config, installNamespace string, bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) error { + // no config, no problem + if config == nil { + return nil + } + + // if the bundle does not support the watchNamespace configuration and it is set, treat it like any unknown field + if config.WatchNamespace != nil && !isWatchNamespaceConfigSupported(bundleInstallModeSet) { + return errors.New(`unknown field "watchNamespace"`) + } + + // if watchNamespace is required then ensure that it is set + if config.WatchNamespace == nil && isWatchNamespaceConfigRequired(bundleInstallModeSet) { + return errors.New(`required field "watchNamespace" is missing`) + } + + // if watchNamespace is set then ensure it is a valid namespace + if config.WatchNamespace != nil { + if errs := validation.IsDNS1123Subdomain(*config.WatchNamespace); len(errs) > 0 { + return fmt.Errorf("invalid 'watchNamespace' %q: namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", *config.WatchNamespace) + } + } + + // only accept install namespace if OwnNamespace install mode is supported + if config.WatchNamespace != nil && *config.WatchNamespace == installNamespace && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}) { + return fmt.Errorf("invalid 'watchNamespace' %q: must not be install namespace (%s)", *config.WatchNamespace, installNamespace) + } + + // only accept non-install namespace is SingleNamespace is supported + if config.WatchNamespace != nil && *config.WatchNamespace != installNamespace && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) { + return fmt.Errorf("invalid 'watchNamespace' %q: must be install namespace (%s)", *config.WatchNamespace, installNamespace) + } + + return nil +} + +// isWatchNamespaceConfigSupported returns true when the bundle exposes a watchNamespace configuration. This happens when: +// - SingleNamespace and/or OwnNamespace install modes are supported +func isWatchNamespaceConfigSupported(bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) bool { + return bundleInstallModeSet.HasAny( + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, + ) +} + +// isWatchNamespaceConfigRequired returns true if the watchNamespace configuration is required. This happens when +// AllNamespaces install mode is not supported and SingleNamespace and/or OwnNamespace is supported +func isWatchNamespaceConfigRequired(bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) bool { + return isWatchNamespaceConfigSupported(bundleInstallModeSet) && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) +} + +// formatUnmarshalError format JSON unmarshal errors to be more readable +func formatUnmarshalError(err error) error { + var unmarshalErr *json.UnmarshalTypeError + if errors.As(err, &unmarshalErr) { + if unmarshalErr.Field == "" { + return errors.New("input is not a valid JSON object") + } else { + return fmt.Errorf("invalid value type for field %q: expected %q but got %q", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value) + } + } + + // unwrap error until the core and process it + for { + unwrapped := errors.Unwrap(err) + if unwrapped == nil { + // usually the errors present in the form json: or yaml: + // we want to extract if we can + errMessageComponents := strings.Split(err.Error(), ":") + coreErrMessage := strings.TrimSpace(errMessageComponents[len(errMessageComponents)-1]) + return errors.New(coreErrMessage) + } + err = unwrapped + } +} diff --git a/internal/operator-controller/rukpak/bundle/config_test.go b/internal/operator-controller/rukpak/bundle/config_test.go new file mode 100644 index 0000000000..a6c4b394a2 --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/config_test.go @@ -0,0 +1,310 @@ +package bundle_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" +) + +func Test_UnmarshalConfig(t *testing.T) { + for _, tc := range []struct { + name string + rawConfig []byte + supportedInstallModes []v1alpha1.InstallModeType + installNamespace string + expectedErrMessage string + expectedConfig *bundle.Config + }{ + { + name: "returns nil for nil config", + rawConfig: nil, + expectedConfig: nil, + }, + { + name: "accepts json config", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts yaml config", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`watchNamespace: some-namespace`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects invalid json", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"hello`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: found unexpected end of stream`, + }, + { + name: "rejects valid json that isn't of object type", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`true`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: input is not a valid JSON object`, + }, + { + name: "rejects additional fields", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`somekey: somevalue`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: unknown field "somekey"`, + }, + { + name: "rejects valid json but invalid registry+v1", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": {"hello": "there"}}`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: invalid value type for field "watchNamespace": expected "string" but got "object"`, + }, + { + name: "rejects bad namespace format", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "bad-Namespace-"}`), + expectedErrMessage: "invalid 'watchNamespace' \"bad-Namespace-\": namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", + }, + { + name: "rejects with unknown field when install modes {AllNamespaces}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "rejects with unknown field when install modes {MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "reject with unknown field when install modes {AllNamespaces, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "reject with required field when install modes {OwnNamespace} and watchNamespace is null", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "reject with required field when install modes {OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "reject with required field when install modes {MultiNamespace, OwnNamespace} and watchNamespace is null", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "reject with required field when install modes {MultiNamespace, OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "accepts when install modes {SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {AllNamespaces, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {MultiNamespace, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "not-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects when install modes {SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "rejects when install modes {AllNamespaces, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "rejects when install modes {MultiNamespace, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "accepts when install modes {AllNamespaces, OwnNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects when install modes {AllNamespaces, OwnNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must be install namespace (not-some-namespace)", + }, + { + name: "rejects with required field error when install modes {SingleNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, MultiNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "accepts null watchNamespace when install modes {AllNamespaces, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "accepts null watchNamespace when install modes {AllNamespaces, OwnNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "accepts no watchNamespace when install modes {AllNamespaces, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "accepts no watchNamespace when install modes {AllNamespaces, OwnNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "rejects with format error when install modes are {SingleNamespace, OwnNamespace} and watchNamespace is ''", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": ""}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"\": namespace must consist of lower case alphanumeric characters", + }, + } { + t.Run(tc.name, func(t *testing.T) { + var rv1 bundle.RegistryV1 + if tc.supportedInstallModes != nil { + rv1 = bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithName("test-operator"). + WithInstallModeSupportFor(tc.supportedInstallModes...). + Build(), + } + } + + config, err := bundle.UnmarshalConfig(tc.rawConfig, rv1, tc.installNamespace) + require.Equal(t, tc.expectedConfig, config) + if tc.expectedErrMessage != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMessage) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/operator-controller/rukpak/bundle/registryv1.go b/internal/operator-controller/rukpak/bundle/registryv1.go index bc757e63d6..cffc374e91 100644 --- a/internal/operator-controller/rukpak/bundle/registryv1.go +++ b/internal/operator-controller/rukpak/bundle/registryv1.go @@ -7,6 +7,10 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" ) +const ( + BundleConfigWatchNamespaceKey = "watchNamespace" +) + type RegistryV1 struct { PackageName string CSV v1alpha1.ClusterServiceVersion diff --git a/internal/operator-controller/rukpak/bundle/source/source.go b/internal/operator-controller/rukpak/bundle/source/source.go index ad8570179e..0f5d3f185c 100644 --- a/internal/operator-controller/rukpak/bundle/source/source.go +++ b/internal/operator-controller/rukpak/bundle/source/source.go @@ -24,6 +24,10 @@ type BundleSource interface { GetBundle() (bundle.RegistryV1, error) } +type RegistryV1Properties struct { + Properties []property.Property `json:"properties"` +} + // identitySource is a bundle source that returns itself type identitySource bundle.RegistryV1 @@ -158,11 +162,7 @@ func copyMetadataPropertiesToCSV(csv *v1alpha1.ClusterServiceVersion, fsys fs.FS // Otherwise, we need to parse the properties.yaml file and // append its properties into the CSV annotation. - type registryV1Properties struct { - Properties []property.Property `json:"properties"` - } - - var metadataProperties registryV1Properties + var metadataProperties RegistryV1Properties if err := yaml.Unmarshal(metadataPropertiesJSON, &metadataProperties); err != nil { return fmt.Errorf("failed to unmarshal metadata/properties.yaml: %w", err) } diff --git a/internal/operator-controller/rukpak/bundle/source/source_test.go b/internal/operator-controller/rukpak/bundle/source/source_test.go index 2ee948638f..45ccafd0dc 100644 --- a/internal/operator-controller/rukpak/bundle/source/source_test.go +++ b/internal/operator-controller/rukpak/bundle/source/source_test.go @@ -2,22 +2,23 @@ package source_test import ( "io/fs" - "strings" "testing" - "testing/fstest" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) const ( olmProperties = "olm.properties" - - bundlePathAnnotations = "metadata/annotations.yaml" - bundlePathProperties = "metadata/properties.yaml" - bundlePathCSV = "manifests/csv.yaml" ) func Test_FromBundle_Success(t *testing.T) { @@ -30,7 +31,18 @@ func Test_FromBundle_Success(t *testing.T) { } func Test_FromFS_Success(t *testing.T) { - rv1, err := source.FromFS(newBundleFS()).GetBundle() + bundleFS := bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("from-file-key", "from-file-value"). + WithBundleResource("csv.yaml", ptr.To(clusterserviceversion.Builder(). + WithName("test.v1.0.0"). + WithAnnotations(map[string]string{ + "olm.properties": `[{"type":"from-csv-annotations-key", "value":"from-csv-annotations-value"}]`, + }). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build())). + Build() + + rv1, err := source.FromFS(bundleFS).GetBundle() require.NoError(t, err) t.Log("Check package name is correctly taken from metadata/annotations.yaml") @@ -47,16 +59,30 @@ func Test_FromFS_Fails(t *testing.T) { }{ { name: "bundle missing ClusterServiceVersion manifest", - FS: removePaths(newBundleFS(), bundlePathCSV), + FS: bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("foo", "bar"). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + }).Build(), }, { name: "bundle missing metadata/annotations.yaml", - FS: removePaths(newBundleFS(), bundlePathAnnotations), + FS: bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleResource("csv.yaml", ptr.To(clusterserviceversion.Builder().Build())).Build(), }, { - name: "bundle missing metadata/ directory", - FS: removePaths(newBundleFS(), "metadata/"), + name: "metadata/annotations.yaml missing package name annotation", + FS: bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleResource("csv.yaml", ptr.To(clusterserviceversion.Builder().Build())).Build(), }, { - name: "bundle missing manifests/ directory", - FS: removePaths(newBundleFS(), "manifests/"), + name: "bundle missing manifests directory", + FS: bundlefs.Builder(). + WithPackageName("test"). + WithBundleProperty("foo", "bar").Build(), }, } { t.Run(tt.name, func(t *testing.T) { @@ -65,47 +91,3 @@ func Test_FromFS_Fails(t *testing.T) { }) } } - -func newBundleFS() fstest.MapFS { - annotationsYml := ` -annotations: - operators.operatorframework.io.bundle.mediatype.v1: registry+v1 - operators.operatorframework.io.bundle.package.v1: test -` - - propertiesYml := ` -properties: - - type: "from-file-key" - value: "from-file-value" -` - - csvYml := ` -apiVersion: operators.operatorframework.io/v1alpha1 -kind: ClusterServiceVersion -metadata: - name: test.v1.0.0 - annotations: - olm.properties: '[{"type":"from-csv-annotations-key", "value":"from-csv-annotations-value"}]' -spec: - installModes: - - type: AllNamespaces - supported: true -` - - return fstest.MapFS{ - bundlePathAnnotations: &fstest.MapFile{Data: []byte(strings.Trim(annotationsYml, "\n"))}, - bundlePathProperties: &fstest.MapFile{Data: []byte(strings.Trim(propertiesYml, "\n"))}, - bundlePathCSV: &fstest.MapFile{Data: []byte(strings.Trim(csvYml, "\n"))}, - } -} - -func removePaths(mapFs fstest.MapFS, paths ...string) fstest.MapFS { - for k := range mapFs { - for _, path := range paths { - if strings.HasPrefix(k, path) { - delete(mapFs, k) - } - } - } - return mapFs -} diff --git a/internal/operator-controller/rukpak/convert/helm.go b/internal/operator-controller/rukpak/convert/helm.go deleted file mode 100644 index 786601fdf8..0000000000 --- a/internal/operator-controller/rukpak/convert/helm.go +++ /dev/null @@ -1,79 +0,0 @@ -package convert - -import ( - "crypto/sha256" - "encoding/json" - "fmt" - - "helm.sh/helm/v3/pkg/chart" - - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" -) - -type BundleToHelmChartConverter struct { - BundleRenderer render.BundleRenderer - CertificateProvider render.CertificateProvider - IsWebhookSupportEnabled bool -} - -func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) { - rv1, err := bundle.GetBundle() - if err != nil { - return nil, err - } - - if len(rv1.CSV.Spec.APIServiceDefinitions.Owned) > 0 { - return nil, fmt.Errorf("unsupported bundle: apiServiceDefintions are not supported") - } - - if len(rv1.CSV.Spec.WebhookDefinitions) > 0 { - if !r.IsWebhookSupportEnabled { - return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported") - } else if r.CertificateProvider == nil { - return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported: certificate provider is nil") - } - } - - if r.CertificateProvider == nil && len(rv1.CSV.Spec.WebhookDefinitions) > 0 { - return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported") - } - - objs, err := r.BundleRenderer.Render( - rv1, installNamespace, - render.WithTargetNamespaces(watchNamespace), - render.WithCertificateProvider(r.CertificateProvider), - ) - - if err != nil { - return nil, fmt.Errorf("error rendering bundle: %w", err) - } - - chrt := &chart.Chart{Metadata: &chart.Metadata{}} - chrt.Metadata.Annotations = rv1.CSV.GetAnnotations() - for _, obj := range objs { - jsonData, err := json.Marshal(obj) - if err != nil { - return nil, err - } - hash := sha256.Sum256(jsonData) - name := fmt.Sprintf("object-%x.json", hash[0:8]) - - // Some registry+v1 manifests may actually contain Go Template strings - // that are meant to survive and actually persist into etcd (e.g. to be - // used as a templated configuration for another component). In order to - // avoid applying templating logic to registry+v1's static manifests, we - // create the manifests as Files, and then template those files via simple - // Templates. - chrt.Files = append(chrt.Files, &chart.File{ - Name: name, - Data: jsonData, - }) - chrt.Templates = append(chrt.Templates, &chart.File{ - Name: name, - Data: []byte(fmt.Sprintf(`{{.Files.Get "%s"}}`, name)), - }) - } - - return chrt, nil -} diff --git a/internal/operator-controller/rukpak/convert/helm_test.go b/internal/operator-controller/rukpak/convert/helm_test.go deleted file mode 100644 index fdfa9812b0..0000000000 --- a/internal/operator-controller/rukpak/convert/helm_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package convert_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/operator-framework/api/pkg/operators/v1alpha1" - - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" - . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" -) - -func Test_BundleToHelmChartConverter_ToHelmChart_ReturnsBundleSourceFailures(t *testing.T) { - converter := convert.BundleToHelmChartConverter{} - var failingBundleSource FakeBundleSource = func() (bundle.RegistryV1, error) { - return bundle.RegistryV1{}, errors.New("some error") - } - _, err := converter.ToHelmChart(failingBundleSource, "install-namespace", "watch-namespace") - require.Error(t, err) - require.Contains(t, err.Error(), "some error") -} - -func Test_BundleToHelmChartConverter_ToHelmChart_ReturnsBundleRendererFailures(t *testing.T) { - converter := convert.BundleToHelmChartConverter{ - BundleRenderer: render.BundleRenderer{ - ResourceGenerators: []render.ResourceGenerator{ - func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { - return nil, errors.New("some error") - }, - }, - }, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), - }, - ) - - _, err := converter.ToHelmChart(b, "install-namespace", "") - require.Error(t, err) - require.Contains(t, err.Error(), "some error") -} - -func Test_BundleToHelmChartConverter_ToHelmChart_NoAPIServiceDefinitions(t *testing.T) { - converter := convert.BundleToHelmChartConverter{} - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV(WithOwnedAPIServiceDescriptions(v1alpha1.APIServiceDescription{})), - }, - ) - - _, err := converter.ToHelmChart(b, "install-namespace", "") - require.Error(t, err) - require.Contains(t, err.Error(), "unsupported bundle: apiServiceDefintions are not supported") -} - -func Test_BundleToHelmChartConverter_ToHelmChart_NoWebhooksWithoutCertProvider(t *testing.T) { - converter := convert.BundleToHelmChartConverter{ - IsWebhookSupportEnabled: true, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV(WithWebhookDefinitions(v1alpha1.WebhookDescription{})), - }, - ) - - _, err := converter.ToHelmChart(b, "install-namespace", "") - require.Error(t, err) - require.Contains(t, err.Error(), "webhookDefinitions are not supported") -} - -func Test_BundleToHelmChartConverter_ToHelmChart_WebhooksSupportDisabled(t *testing.T) { - converter := convert.BundleToHelmChartConverter{ - IsWebhookSupportEnabled: false, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV(WithWebhookDefinitions(v1alpha1.WebhookDescription{})), - }, - ) - - _, err := converter.ToHelmChart(b, "install-namespace", "") - require.Error(t, err) - require.Contains(t, err.Error(), "webhookDefinitions are not supported") -} - -func Test_BundleToHelmChartConverter_ToHelmChart_WebhooksWithCertProvider(t *testing.T) { - converter := convert.BundleToHelmChartConverter{ - CertificateProvider: FakeCertProvider{}, - IsWebhookSupportEnabled: true, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), - WithWebhookDefinitions(v1alpha1.WebhookDescription{}), - ), - }, - ) - - _, err := converter.ToHelmChart(b, "install-namespace", "") - require.NoError(t, err) -} - -func Test_BundleToHelmChartConverter_ToHelmChart_BundleRendererIntegration(t *testing.T) { - expectedInstallNamespace := "install-namespace" - expectedWatchNamespace := "" - expectedCertProvider := FakeCertProvider{} - - converter := convert.BundleToHelmChartConverter{ - BundleRenderer: render.BundleRenderer{ - ResourceGenerators: []render.ResourceGenerator{ - func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { - // ensure correct options are being passed down to the bundle renderer - require.Equal(t, expectedInstallNamespace, opts.InstallNamespace) - require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces) - require.Equal(t, expectedCertProvider, opts.CertificateProvider) - return nil, nil - }, - }, - }, - CertificateProvider: expectedCertProvider, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), - }, - ) - - _, err := converter.ToHelmChart(b, expectedInstallNamespace, expectedWatchNamespace) - require.NoError(t, err) -} - -func Test_BundleToHelmChartConverter_ToHelmChart_Success(t *testing.T) { - converter := convert.BundleToHelmChartConverter{ - BundleRenderer: render.BundleRenderer{ - ResourceGenerators: []render.ResourceGenerator{ - func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { - out := make([]client.Object, 0, len(rv1.Others)) - for i := range rv1.Others { - out = append(out, &rv1.Others[i]) - } - return out, nil - }, - }, - }, - } - - b := source.FromBundle( - bundle.RegistryV1{ - CSV: MakeCSV( - WithAnnotations(map[string]string{"foo": "bar"}), - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), - ), - Others: []unstructured.Unstructured{ - *ToUnstructuredT(t, &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: corev1.SchemeGroupVersion.String(), - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "testService", - }, - }), - }, - }, - ) - - chart, err := converter.ToHelmChart(b, "install-namespace", "") - require.NoError(t, err) - require.NotNil(t, chart) - require.NotNil(t, chart.Metadata) - - t.Log("Check Chart metadata contains CSV annotations") - require.Equal(t, map[string]string{"foo": "bar"}, chart.Metadata.Annotations) - - t.Log("Check Chart templates have the same number of resources generated by the renderer") - require.Len(t, chart.Templates, 1) -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator.go deleted file mode 100644 index 4678b2de06..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator.go +++ /dev/null @@ -1,171 +0,0 @@ -// Originally copied from https://github.com/carvel-dev/kapp/tree/d7fc2e15439331aa3a379485bb124e91a0829d2e -// Attribution: -// Copyright 2024 The Carvel Authors. -// SPDX-License-Identifier: Apache-2.0 - -package crdupgradesafety - -import ( - "errors" - "fmt" - "maps" - "reflect" - "slices" - - "github.com/openshift/crd-schema-checker/pkg/manifestcomparators" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// ChangeValidation is a function that accepts a FieldDiff -// as a parameter and should return: -// - a boolean representation of whether or not the change -// - an error if the change would be unsafe -// has been fully handled (i.e no additional changes exist) -type ChangeValidation func(diff FieldDiff) (bool, error) - -// ChangeValidator is a Validation implementation focused on -// handling updates to existing fields in a CRD -type ChangeValidator struct { - // Validations is a slice of ChangeValidations - // to run against each changed field - Validations []ChangeValidation -} - -func (cv *ChangeValidator) Name() string { - return "ChangeValidator" -} - -// Validate will compare each version in the provided existing and new CRDs. -// Since the ChangeValidator is tailored to handling updates to existing fields in -// each version of a CRD. As such the following is assumed: -// - Validating the removal of versions during an update is handled outside of this -// validator. If a version in the existing version of the CRD does not exist in the new -// version that version of the CRD is skipped in this validator. -// - Removal of existing fields is unsafe. Regardless of whether or not this is handled -// by a validator outside this one, if a field is present in a version provided by the existing CRD -// but not present in the same version provided by the new CRD this validation will fail. -// -// Additionally, any changes that are not validated and handled by the known ChangeValidations -// are deemed as unsafe and returns an error. -func (cv *ChangeValidator) Validate(old, new apiextensionsv1.CustomResourceDefinition) error { - errs := []error{} - for _, version := range old.Spec.Versions { - newVersion := manifestcomparators.GetVersionByName(&new, version.Name) - if newVersion == nil { - // if the new version doesn't exist skip this version - continue - } - flatOld := FlattenSchema(version.Schema.OpenAPIV3Schema) - flatNew := FlattenSchema(newVersion.Schema.OpenAPIV3Schema) - - diffs, err := CalculateFlatSchemaDiff(flatOld, flatNew) - if err != nil { - errs = append(errs, fmt.Errorf("calculating schema diff for CRD version %q", version.Name)) - continue - } - - for _, field := range slices.Sorted(maps.Keys(diffs)) { - diff := diffs[field] - - handled := false - for _, validation := range cv.Validations { - ok, err := validation(diff) - if err != nil { - errs = append(errs, fmt.Errorf("version %q, field %q: %w", version.Name, field, err)) - } - if ok { - handled = true - break - } - } - - if !handled { - errs = append(errs, fmt.Errorf("version %q, field %q has unknown change, refusing to determine that change is safe", version.Name, field)) - } - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -type FieldDiff struct { - Old *apiextensionsv1.JSONSchemaProps - New *apiextensionsv1.JSONSchemaProps -} - -// FlatSchema is a flat representation of a CRD schema. -type FlatSchema map[string]*apiextensionsv1.JSONSchemaProps - -// FlattenSchema takes in a CRD version OpenAPIV3Schema and returns -// a flattened representation of it. For example, a CRD with a schema of: -// ```yaml -// -// ... -// spec: -// type: object -// properties: -// foo: -// type: string -// bar: -// type: string -// ... -// -// ``` -// would be represented as: -// -// map[string]*apiextensionsv1.JSONSchemaProps{ -// "^": {}, -// "^.spec": {}, -// "^.spec.foo": {}, -// "^.spec.bar": {}, -// } -// -// where "^" represents the "root" schema -func FlattenSchema(schema *apiextensionsv1.JSONSchemaProps) FlatSchema { - fieldMap := map[string]*apiextensionsv1.JSONSchemaProps{} - - manifestcomparators.SchemaHas(schema, - field.NewPath("^"), - field.NewPath("^"), - nil, - func(s *apiextensionsv1.JSONSchemaProps, _, simpleLocation *field.Path, _ []*apiextensionsv1.JSONSchemaProps) bool { - fieldMap[simpleLocation.String()] = s.DeepCopy() - return false - }) - - return fieldMap -} - -// CalculateFlatSchemaDiff finds fields in a FlatSchema that are different -// and returns a mapping of field --> old and new field schemas. If a field -// exists in the old FlatSchema but not the new an empty diff mapping and an error is returned. -func CalculateFlatSchemaDiff(o, n FlatSchema) (map[string]FieldDiff, error) { - diffMap := map[string]FieldDiff{} - for field, schema := range o { - if _, ok := n[field]; !ok { - return diffMap, fmt.Errorf("field %q in existing not found in new", field) - } - newSchema := n[field] - - // Copy the schemas and remove any child properties for comparison. - // In theory this will focus in on detecting changes for only the - // field we are looking at and ignore changes in the children fields. - // Since we are iterating through the map that should have all fields - // we should still detect changes in the children fields. - oldCopy := schema.DeepCopy() - newCopy := newSchema.DeepCopy() - oldCopy.Properties, oldCopy.Items = nil, nil - newCopy.Properties, newCopy.Items = nil, nil - if !reflect.DeepEqual(oldCopy, newCopy) { - diffMap[field] = FieldDiff{ - Old: oldCopy, - New: newCopy, - } - } - } - return diffMap, nil -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator_test.go deleted file mode 100644 index cc12bc5c1c..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/change_validator_test.go +++ /dev/null @@ -1,338 +0,0 @@ -// Originally copied from https://github.com/carvel-dev/kapp/tree/d7fc2e15439331aa3a379485bb124e91a0829d2e -// Attribution: -// Copyright 2024 The Carvel Authors. -// SPDX-License-Identifier: Apache-2.0 - -package crdupgradesafety_test - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" -) - -func TestCalculateFlatSchemaDiff(t *testing.T) { - for _, tc := range []struct { - name string - old crdupgradesafety.FlatSchema - new crdupgradesafety.FlatSchema - expectedDiff map[string]crdupgradesafety.FieldDiff - shouldError bool - }{ - { - name: "no diff in schemas, empty diff, no error", - old: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{}, - }, - new: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{}, - }, - expectedDiff: map[string]crdupgradesafety.FieldDiff{}, - }, - { - name: "diff in schemas, diff returned, no error", - old: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{}, - }, - new: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - expectedDiff: map[string]crdupgradesafety.FieldDiff{ - "foo": { - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ID: "bar"}, - }, - }, - }, - { - name: "diff in child properties only, no diff returned, no error", - old: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{ - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "bar": {ID: "bar"}, - }, - }, - }, - new: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{ - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "bar": {ID: "baz"}, - }, - }, - }, - expectedDiff: map[string]crdupgradesafety.FieldDiff{}, - }, - { - name: "diff in child items only, no diff returned, no error", - old: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{ - Items: &apiextensionsv1.JSONSchemaPropsOrArray{Schema: &apiextensionsv1.JSONSchemaProps{ID: "bar"}}, - }, - }, - new: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{ - Items: &apiextensionsv1.JSONSchemaPropsOrArray{Schema: &apiextensionsv1.JSONSchemaProps{ID: "baz"}}, - }, - }, - expectedDiff: map[string]crdupgradesafety.FieldDiff{}, - }, - { - name: "field exists in old but not new, no diff returned, error", - old: crdupgradesafety.FlatSchema{ - "foo": &apiextensionsv1.JSONSchemaProps{}, - }, - new: crdupgradesafety.FlatSchema{ - "bar": &apiextensionsv1.JSONSchemaProps{}, - }, - expectedDiff: map[string]crdupgradesafety.FieldDiff{}, - shouldError: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - diff, err := crdupgradesafety.CalculateFlatSchemaDiff(tc.old, tc.new) - assert.Equal(t, tc.shouldError, err != nil, "should error? - %v", tc.shouldError) - assert.Equal(t, tc.expectedDiff, diff) - }) - } -} - -func TestFlattenSchema(t *testing.T) { - schema := &apiextensionsv1.JSONSchemaProps{ - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "foo": { - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "bar": {}, - }, - }, - "baz": {}, - }, - } - - foo := schema.Properties["foo"] - foobar := schema.Properties["foo"].Properties["bar"] - baz := schema.Properties["baz"] - expected := crdupgradesafety.FlatSchema{ - "^": schema, - "^.foo": &foo, - "^.foo.bar": &foobar, - "^.baz": &baz, - } - - actual := crdupgradesafety.FlattenSchema(schema) - - assert.Equal(t, expected, actual) -} - -func TestChangeValidator(t *testing.T) { - validationErr1 := errors.New(`version "v1alpha1", field "^" has unknown change, refusing to determine that change is safe`) - validationErr2 := errors.New(`version "v1alpha1", field "^": fail`) - - for _, tc := range []struct { - name string - changeValidator *crdupgradesafety.ChangeValidator - old apiextensionsv1.CustomResourceDefinition - new apiextensionsv1.CustomResourceDefinition - expectedError error - }{ - { - name: "no changes, no error", - changeValidator: &crdupgradesafety.ChangeValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("should not run") - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - }, - { - name: "changes, validation successful, change is fully handled, no error", - changeValidator: &crdupgradesafety.ChangeValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return true, nil - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - { - name: "changes, validation successful, change not fully handled, error", - changeValidator: &crdupgradesafety.ChangeValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, nil - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: validationErr1, - }, - { - name: "changes, validation failed, change fully handled, error", - changeValidator: &crdupgradesafety.ChangeValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return true, errors.New("fail") - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: validationErr2, - }, - { - name: "changes, validation failed, change not fully handled, ordered error", - changeValidator: &crdupgradesafety.ChangeValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("fail") - }, - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("error") - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: fmt.Errorf("%w\n%s\n%w", validationErr2, `version "v1alpha1", field "^": error`, validationErr1), - }, - } { - t.Run(tc.name, func(t *testing.T) { - err := tc.changeValidator.Validate(tc.old, tc.new) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go deleted file mode 100644 index 669f65e57e..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks.go +++ /dev/null @@ -1,244 +0,0 @@ -package crdupgradesafety - -import ( - "bytes" - "cmp" - "fmt" - "reflect" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/util/sets" -) - -type resetFunc func(diff FieldDiff) FieldDiff - -func isHandled(diff FieldDiff, reset resetFunc) bool { - diff = reset(diff) - return reflect.DeepEqual(diff.Old, diff.New) -} - -func Enum(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Enum = []apiextensionsv1.JSON{} - diff.New.Enum = []apiextensionsv1.JSON{} - return diff - } - - oldEnums := sets.New[string]() - for _, json := range diff.Old.Enum { - oldEnums.Insert(string(json.Raw)) - } - - newEnums := sets.New[string]() - for _, json := range diff.New.Enum { - newEnums.Insert(string(json.Raw)) - } - diffEnums := oldEnums.Difference(newEnums) - var err error - - switch { - case oldEnums.Len() == 0 && newEnums.Len() > 0: - err = fmt.Errorf("enum constraints %v added when there were no restrictions previously", newEnums.UnsortedList()) - case diffEnums.Len() > 0: - err = fmt.Errorf("enums %v removed from the set of previously allowed values", diffEnums.UnsortedList()) - } - - return isHandled(diff, reset), err -} - -func Required(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Required = []string{} - diff.New.Required = []string{} - return diff - } - - oldRequired := sets.New(diff.Old.Required...) - newRequired := sets.New(diff.New.Required...) - diffRequired := newRequired.Difference(oldRequired) - var err error - - if diffRequired.Len() > 0 { - err = fmt.Errorf("new required fields %v added", diffRequired.UnsortedList()) - } - - return isHandled(diff, reset), err -} - -func maxVerification[T cmp.Ordered](older *T, newer *T) error { - var err error - switch { - case older == nil && newer != nil: - err = fmt.Errorf("constraint %v added when there were no restrictions previously", *newer) - case older != nil && newer != nil && *newer < *older: - err = fmt.Errorf("constraint decreased from %v to %v", *older, *newer) - } - return err -} - -func Maximum(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Maximum = nil - diff.New.Maximum = nil - return diff - } - - err := maxVerification(diff.Old.Maximum, diff.New.Maximum) - if err != nil { - err = fmt.Errorf("maximum: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MaxItems(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MaxItems = nil - diff.New.MaxItems = nil - return diff - } - - err := maxVerification(diff.Old.MaxItems, diff.New.MaxItems) - if err != nil { - err = fmt.Errorf("maxItems: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MaxLength(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MaxLength = nil - diff.New.MaxLength = nil - return diff - } - - err := maxVerification(diff.Old.MaxLength, diff.New.MaxLength) - if err != nil { - err = fmt.Errorf("maxLength: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MaxProperties(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MaxProperties = nil - diff.New.MaxProperties = nil - return diff - } - - err := maxVerification(diff.Old.MaxProperties, diff.New.MaxProperties) - if err != nil { - err = fmt.Errorf("maxProperties: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func minVerification[T cmp.Ordered](older *T, newer *T) error { - var err error - switch { - case older == nil && newer != nil: - err = fmt.Errorf("constraint %v added when there were no restrictions previously", *newer) - case older != nil && newer != nil && *newer > *older: - err = fmt.Errorf("constraint increased from %v to %v", *older, *newer) - } - return err -} - -func Minimum(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Minimum = nil - diff.New.Minimum = nil - return diff - } - - err := minVerification(diff.Old.Minimum, diff.New.Minimum) - if err != nil { - err = fmt.Errorf("minimum: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MinItems(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MinItems = nil - diff.New.MinItems = nil - return diff - } - - err := minVerification(diff.Old.MinItems, diff.New.MinItems) - if err != nil { - err = fmt.Errorf("minItems: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MinLength(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MinLength = nil - diff.New.MinLength = nil - return diff - } - - err := minVerification(diff.Old.MinLength, diff.New.MinLength) - if err != nil { - err = fmt.Errorf("minLength: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func MinProperties(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.MinProperties = nil - diff.New.MinProperties = nil - return diff - } - - err := minVerification(diff.Old.MinProperties, diff.New.MinProperties) - if err != nil { - err = fmt.Errorf("minProperties: %s", err.Error()) - } - - return isHandled(diff, reset), err -} - -func Default(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Default = nil - diff.New.Default = nil - return diff - } - - var err error - - switch { - case diff.Old.Default == nil && diff.New.Default != nil: - err = fmt.Errorf("default value %q added when there was no default previously", string(diff.New.Default.Raw)) - case diff.Old.Default != nil && diff.New.Default == nil: - err = fmt.Errorf("default value %q removed", string(diff.Old.Default.Raw)) - case diff.Old.Default != nil && diff.New.Default != nil && !bytes.Equal(diff.Old.Default.Raw, diff.New.Default.Raw): - err = fmt.Errorf("default value changed from %q to %q", string(diff.Old.Default.Raw), string(diff.New.Default.Raw)) - } - - return isHandled(diff, reset), err -} - -func Type(diff FieldDiff) (bool, error) { - reset := func(diff FieldDiff) FieldDiff { - diff.Old.Type = "" - diff.New.Type = "" - return diff - } - - var err error - if diff.Old.Type != diff.New.Type { - err = fmt.Errorf("type changed from %q to %q", diff.Old.Type, diff.New.Type) - } - - return isHandled(diff, reset), err -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go deleted file mode 100644 index 36618b584a..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/checks_test.go +++ /dev/null @@ -1,906 +0,0 @@ -package crdupgradesafety - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/utils/ptr" -) - -type testcase struct { - name string - diff FieldDiff - err error - handled bool -} - -func TestEnum(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - }, - }, - }, - err: nil, - handled: true, - }, - { - name: "new enum constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{}, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - }, - }, - }, - err: errors.New("enum constraints [foo] added when there were no restrictions previously"), - handled: true, - }, - { - name: "remove enum value, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - { - Raw: []byte("bar"), - }, - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("bar"), - }, - }, - }, - }, - err: errors.New("enums [foo] removed from the set of previously allowed values"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - { - name: "different field changed with enum, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - Enum: []apiextensionsv1.JSON{ - { - Raw: []byte("foo"), - }, - }, - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Enum(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestRequired(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Required: []string{ - "foo", - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Required: []string{ - "foo", - }, - }, - }, - err: nil, - handled: true, - }, - { - name: "new required field, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - Required: []string{ - "foo", - }, - }, - }, - err: errors.New("new required fields [foo] added"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Required(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMaximum(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(10.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(10.0), - }, - }, - err: nil, - handled: true, - }, - { - name: "new maximum constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(10.0), - }, - }, - err: errors.New("maximum: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "maximum constraint decreased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(20.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(10.0), - }, - }, - err: errors.New("maximum: constraint decreased from 20 to 10"), - handled: true, - }, - { - name: "maximum constraint increased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(20.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Maximum: ptr.To(30.0), - }, - }, - err: nil, - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Maximum(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMaxItems(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new maxItems constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(10)), - }, - }, - err: errors.New("maxItems: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "maxItems constraint decreased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(10)), - }, - }, - err: errors.New("maxItems: constraint decreased from 20 to 10"), - handled: true, - }, - { - name: "maxitems constraint increased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxItems: ptr.To(int64(20)), - }, - }, - err: nil, - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MaxItems(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMaxLength(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new maxLength constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(10)), - }, - }, - err: errors.New("maxLength: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "maxLength constraint decreased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(10)), - }, - }, - err: errors.New("maxLength: constraint decreased from 20 to 10"), - handled: true, - }, - { - name: "maxLength constraint increased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxLength: ptr.To(int64(20)), - }, - }, - err: nil, - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MaxLength(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMaxProperties(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new maxProperties constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(10)), - }, - }, - err: errors.New("maxProperties: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "maxProperties constraint decreased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(10)), - }, - }, - err: errors.New("maxProperties: constraint decreased from 20 to 10"), - handled: true, - }, - { - name: "maxProperties constraint increased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MaxProperties: ptr.To(int64(20)), - }, - }, - err: nil, - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MaxProperties(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMinItems(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new minItems constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(10)), - }, - }, - err: errors.New("minItems: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "minItems constraint decreased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "minItems constraint increased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinItems: ptr.To(int64(20)), - }, - }, - err: errors.New("minItems: constraint increased from 10 to 20"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MinItems(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMinimum(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(10.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(10.0), - }, - }, - err: nil, - handled: true, - }, - { - name: "new minimum constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(10.0), - }, - }, - err: errors.New("minimum: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "minLength constraint decreased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(20.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(10.0), - }, - }, - err: nil, - handled: true, - }, - { - name: "minLength constraint increased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(10.0), - }, - New: &apiextensionsv1.JSONSchemaProps{ - Minimum: ptr.To(20.0), - }, - }, - err: errors.New("minimum: constraint increased from 10 to 20"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Minimum(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMinLength(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new minLength constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(10)), - }, - }, - err: errors.New("minLength: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "minLength constraint decreased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "minLength constraint increased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinLength: ptr.To(int64(20)), - }, - }, - err: errors.New("minLength: constraint increased from 10 to 20"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MinLength(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestMinProperties(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "new minProperties constraint, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(10)), - }, - }, - err: errors.New("minProperties: constraint 10 added when there were no restrictions previously"), - handled: true, - }, - { - name: "minProperties constraint decreased, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(20)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(10)), - }, - }, - err: nil, - handled: true, - }, - { - name: "minProperties constraint increased, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(10)), - }, - New: &apiextensionsv1.JSONSchemaProps{ - MinProperties: ptr.To(int64(20)), - }, - }, - err: errors.New("minProperties: constraint increased from 10 to 20"), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := MinProperties(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestDefault(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("foo"), - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("foo"), - }, - }, - }, - err: nil, - handled: true, - }, - { - name: "new default value, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{}, - New: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("foo"), - }, - }, - }, - err: errors.New("default value \"foo\" added when there was no default previously"), - handled: true, - }, - { - name: "default value removed, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("foo"), - }, - }, - New: &apiextensionsv1.JSONSchemaProps{}, - }, - err: errors.New("default value \"foo\" removed"), - handled: true, - }, - { - name: "default value changed, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("foo"), - }, - }, - New: &apiextensionsv1.JSONSchemaProps{ - Default: &apiextensionsv1.JSON{ - Raw: []byte("bar"), - }, - }, - }, - err: errors.New("default value changed from \"foo\" to \"bar\""), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Default(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} - -func TestType(t *testing.T) { - for _, tc := range []testcase{ - { - name: "no diff, no error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Type: "string", - }, - New: &apiextensionsv1.JSONSchemaProps{ - Type: "string", - }, - }, - err: nil, - handled: true, - }, - { - name: "type changed, error, handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - Type: "string", - }, - New: &apiextensionsv1.JSONSchemaProps{ - Type: "integer", - }, - }, - err: errors.New("type changed from \"string\" to \"integer\""), - handled: true, - }, - { - name: "different field changed, no error, not handled", - diff: FieldDiff{ - Old: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - New: &apiextensionsv1.JSONSchemaProps{ - ID: "bar", - }, - }, - err: nil, - handled: false, - }, - } { - t.Run(tc.name, func(t *testing.T) { - handled, err := Type(tc.diff) - require.Equal(t, tc.err, err) - require.Equal(t, tc.handled, handled) - }) - } -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go index 6bc177cd1b..e7830ce620 100644 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety.go @@ -6,56 +6,43 @@ import ( "fmt" "strings" - "helm.sh/helm/v3/pkg/release" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/crdify/pkg/config" + "sigs.k8s.io/crdify/pkg/runner" + "sigs.k8s.io/crdify/pkg/validations" + "sigs.k8s.io/crdify/pkg/validations/property" ) type Option func(p *Preflight) -func WithValidator(v *Validator) Option { +func WithConfig(cfg *config.Config) Option { return func(p *Preflight) { - p.validator = v + p.config = cfg + } +} + +func WithRegistry(reg validations.Registry) Option { + return func(p *Preflight) { + p.registry = reg } } type Preflight struct { crdClient apiextensionsv1client.CustomResourceDefinitionInterface - validator *Validator + config *config.Config + registry validations.Registry } func NewPreflight(crdCli apiextensionsv1client.CustomResourceDefinitionInterface, opts ...Option) *Preflight { - changeValidations := []ChangeValidation{ - Enum, - Required, - Maximum, - MaxItems, - MaxLength, - MaxProperties, - Minimum, - MinItems, - MinLength, - MinProperties, - Default, - Type, - } p := &Preflight{ crdClient: crdCli, - // create a default validator. Can be overridden via the options - validator: &Validator{ - Validations: []Validation{ - NewValidationFunc("NoScopeChange", NoScopeChange), - NewValidationFunc("NoStoredVersionRemoved", NoStoredVersionRemoved), - NewValidationFunc("NoExistingFieldRemoved", NoExistingFieldRemoved), - &ServedVersionValidator{Validations: changeValidations}, - &ChangeValidator{Validations: changeValidations}, - }, - }, + config: defaultConfig(), + registry: defaultRegistry(), } for _, o := range opts { @@ -65,22 +52,21 @@ func NewPreflight(crdCli apiextensionsv1client.CustomResourceDefinitionInterface return p } -func (p *Preflight) Install(ctx context.Context, rel *release.Release) error { - return p.runPreflight(ctx, rel) +func (p *Preflight) Install(ctx context.Context, objs []client.Object) error { + return p.runPreflight(ctx, objs) } -func (p *Preflight) Upgrade(ctx context.Context, rel *release.Release) error { - return p.runPreflight(ctx, rel) +func (p *Preflight) Upgrade(ctx context.Context, objs []client.Object) error { + return p.runPreflight(ctx, objs) } -func (p *Preflight) runPreflight(ctx context.Context, rel *release.Release) error { - if rel == nil { +func (p *Preflight) runPreflight(ctx context.Context, relObjects []client.Object) error { + if len(relObjects) == 0 { return nil } - - relObjects, err := util.ManifestObjects(strings.NewReader(rel.Manifest), fmt.Sprintf("%s-release-manifest", rel.Name)) + runner, err := runner.New(p.config, p.registry) if err != nil { - return fmt.Errorf("parsing release %q objects: %w", rel.Name, err) + return fmt.Errorf("creating CRD validation runner: %w", err) } validateErrors := make([]error, 0, len(relObjects)) @@ -109,11 +95,237 @@ func (p *Preflight) runPreflight(ctx context.Context, rel *release.Release) erro return fmt.Errorf("getting existing resource for CRD %q: %w", newCrd.Name, err) } - err = p.validator.Validate(*oldCrd, *newCrd) - if err != nil { - validateErrors = append(validateErrors, fmt.Errorf("validating upgrade for CRD %q failed: %w", newCrd.Name, err)) + results := runner.Run(oldCrd, newCrd) + if results.HasFailures() { + resultErrs := crdWideErrors(results) + resultErrs = append(resultErrs, sameVersionErrors(results)...) + resultErrs = append(resultErrs, servedVersionErrors(results)...) + if len(resultErrs) > 0 { + validateErrors = append(validateErrors, fmt.Errorf("validating upgrade for CRD %q: %w", newCrd.Name, errors.Join(resultErrs...))) + } } } return errors.Join(validateErrors...) } + +func defaultConfig() *config.Config { + return &config.Config{ + // Ignore served version validations if conversion policy is set. + Conversion: config.ConversionPolicyIgnore, + // Fail-closed by default + UnhandledEnforcement: config.EnforcementPolicyError, + // Use the default validation configurations as they are + // the strictest possible. + Validations: []config.ValidationConfig{ + // Do not enforce the description validation + // because OLM should not block on field description changes. + { + Name: "description", + Enforcement: config.EnforcementPolicyNone, + }, + { + Name: "enum", + Enforcement: config.EnforcementPolicyError, + Configuration: map[string]interface{}{ + "additionPolicy": property.AdditionPolicyAllow, + }, + }, + }, + } +} + +func defaultRegistry() validations.Registry { + return runner.DefaultRegistry() +} + +func crdWideErrors(results *runner.Results) []error { + if results == nil { + return nil + } + + errs := []error{} + for _, result := range results.CRDValidation { + for _, err := range result.Errors { + errs = append(errs, fmt.Errorf("%s: %s", result.Name, err)) + } + } + + return errs +} + +func sameVersionErrors(results *runner.Results) []error { + if results == nil { + return nil + } + + errs := []error{} + for version, propertyResults := range results.SameVersionValidation { + for property, comparisonResults := range propertyResults { + for _, result := range comparisonResults { + for _, err := range result.Errors { + msg := err + if result.Name == "unhandled" { + msg = conciseUnhandledMessage(err) + } + errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, msg)) + } + } + } + } + + return errs +} + +func servedVersionErrors(results *runner.Results) []error { + if results == nil { + return nil + } + + errs := []error{} + for version, propertyResults := range results.ServedVersionValidation { + for property, comparisonResults := range propertyResults { + for _, result := range comparisonResults { + for _, err := range result.Errors { + msg := err + if result.Name == "unhandled" { + msg = conciseUnhandledMessage(err) + } + errs = append(errs, fmt.Errorf("%s: %s: %s: %s", version, property, result.Name, msg)) + } + } + } + } + + return errs +} + +const unhandledSummaryPrefix = "unhandled changes found" + +// conciseUnhandledMessage trims the CRD diff emitted by crdify's "unhandled" comparator +// into a short human readable description so operators get a hint of the change without +// the unreadable Go struct dump. +func conciseUnhandledMessage(raw string) string { + if !strings.Contains(raw, unhandledSummaryPrefix) { + return raw + } + + details := extractUnhandledDetails(raw) + if len(details) == 0 { + return unhandledSummaryPrefix + } + + return fmt.Sprintf("%s (%s)", unhandledSummaryPrefix, strings.Join(details, "; ")) +} + +func extractUnhandledDetails(raw string) []string { + type diffEntry struct { + before string + after string + beforeRaw string + afterRaw string + } + + entries := map[string]*diffEntry{} + order := []string{} + + for _, line := range strings.Split(raw, "\n") { + trimmed := strings.TrimSpace(line) + if len(trimmed) < 2 { + continue + } + + sign := trimmed[0] + if sign != '-' && sign != '+' { + continue + } + + field, value, rawValue := parseUnhandledDiffValue(trimmed[1:]) + if field == "" { + continue + } + + entry, ok := entries[field] + if !ok { + entry = &diffEntry{} + entries[field] = entry + order = append(order, field) + } + + if sign == '-' { + entry.before = value + entry.beforeRaw = rawValue + } else { + entry.after = value + entry.afterRaw = rawValue + } + } + + details := []string{} + for _, field := range order { + entry := entries[field] + if entry.before == "" && entry.after == "" { + continue + } + if entry.before == entry.after && entry.beforeRaw == entry.afterRaw { + continue + } + + before := entry.before + if before == "" { + before = "" + } + after := entry.after + if after == "" { + after = "" + } + if entry.before == entry.after && entry.beforeRaw != entry.afterRaw { + after = after + " (changed)" + } + + details = append(details, fmt.Sprintf("%s %s -> %s", field, before, after)) + } + + return details +} + +func parseUnhandledDiffValue(fragment string) (string, string, string) { + cleaned := strings.TrimSpace(fragment) + cleaned = strings.TrimPrefix(cleaned, "\t") + cleaned = strings.TrimSpace(cleaned) + cleaned = strings.TrimSuffix(cleaned, ",") + + parts := strings.SplitN(cleaned, ":", 2) + if len(parts) != 2 { + return "", "", "" + } + + field := strings.TrimSpace(parts[0]) + rawValue := strings.TrimSpace(parts[1]) + value := normalizeUnhandledValue(rawValue) + + if field == "" { + return "", "", "" + } + + return field, value, rawValue +} + +func normalizeUnhandledValue(value string) string { + value = strings.TrimSuffix(value, ",") + value = strings.TrimSpace(value) + + switch value { + case "": + return "" + case "\"\"": + return "\"\"" + } + + value = strings.ReplaceAll(value, "v1.", "") + if strings.Contains(value, "JSONSchemaProps") { + return "" + } + + return value +} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety_test.go index 12241bd7f8..9fb275880d 100644 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety_test.go +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/crdupgradesafety_test.go @@ -15,7 +15,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + crdifyconfig "sigs.k8s.io/crdify/pkg/config" + "github.com/operator-framework/operator-controller/internal/operator-controller/applier" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" ) @@ -30,18 +32,15 @@ func (c *MockCRDGetter) Get(ctx context.Context, name string, options metav1.Get return c.oldCrd, c.getErr } -func newMockPreflight(crd *apiextensionsv1.CustomResourceDefinition, err error, customValidator *crdupgradesafety.Validator) *crdupgradesafety.Preflight { +func newMockPreflight(crd *apiextensionsv1.CustomResourceDefinition, err error) *crdupgradesafety.Preflight { var preflightOpts []crdupgradesafety.Option - if customValidator != nil { - preflightOpts = append(preflightOpts, crdupgradesafety.WithValidator(customValidator)) - } return crdupgradesafety.NewPreflight(&MockCRDGetter{ oldCrd: crd, getErr: err, }, preflightOpts...) } -const crdFolder string = "../../../../../testdata/manifests" +const crdFolder string = "testdata/manifests" func getCrdFromManifestFile(t *testing.T, oldCrdFile string) *apiextensionsv1.CustomResourceDefinition { if oldCrdFile == "" { @@ -69,15 +68,22 @@ func getManifestString(t *testing.T, crdFile string) string { return string(buff) } +func wantErrorMsgs(wantMsgs []string) require.ErrorAssertionFunc { + return func(t require.TestingT, haveErr error, _ ...interface{}) { + for _, wantMsg := range wantMsgs { + require.ErrorContains(t, haveErr, wantMsg) + } + } +} + // TestInstall exists only for completeness as Install() is currently a no-op. It can be used as // a template for real tests in the future if the func is implemented. func TestInstall(t *testing.T) { tests := []struct { name string oldCrdPath string - validator *crdupgradesafety.Validator release *release.Release - wantErrMsgs []string + requireErr require.ErrorAssertionFunc wantCrdGetErr error }{ { @@ -95,7 +101,7 @@ func TestInstall(t *testing.T) { Name: "test-release", Manifest: "abcd", }, - wantErrMsgs: []string{"json: cannot unmarshal string into Go value of type unstructured.detector"}, + requireErr: wantErrorMsgs([]string{"json: cannot unmarshal string into Go value of type unstructured.detector"}), }, { name: "release with no CRD objects", @@ -111,7 +117,7 @@ func TestInstall(t *testing.T) { Manifest: getManifestString(t, "crd-valid-upgrade.json"), }, wantCrdGetErr: fmt.Errorf("error!"), - wantErrMsgs: []string{"error!"}, + requireErr: wantErrorMsgs([]string{"error!"}), }, { name: "fail to get old crd, not found error", @@ -127,23 +133,7 @@ func TestInstall(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-invalid"), }, - wantErrMsgs: []string{"json: cannot unmarshal"}, - }, - { - name: "custom validator", - oldCrdPath: "old-crd.json", - release: &release.Release{ - Name: "test-release", - Manifest: getManifestString(t, "old-crd.json"), - }, - validator: &crdupgradesafety.Validator{ - Validations: []crdupgradesafety.Validation{ - crdupgradesafety.NewValidationFunc("test", func(old, new apiextensionsv1.CustomResourceDefinition) error { - return fmt.Errorf("custom validation error!!") - }), - }, - }, - wantErrMsgs: []string{"custom validation error!!"}, + requireErr: wantErrorMsgs([]string{"json: cannot unmarshal"}), }, { name: "valid upgrade", @@ -162,21 +152,21 @@ func TestInstall(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-invalid-upgrade.json"), }, - wantErrMsgs: []string{ - `"NoScopeChange"`, - `"NoStoredVersionRemoved"`, - `enum constraints`, - `new required fields`, - `maximum: constraint`, - `maxItems: constraint`, - `maxLength: constraint`, - `maxProperties: constraint`, - `minimum: constraint`, - `minItems: constraint`, - `minLength: constraint`, - `minProperties: constraint`, - `default value`, - }, + requireErr: wantErrorMsgs([]string{ + `scope:`, + `storedVersionRemoval:`, + `enum:`, + `required:`, + `maximum:`, + `maxItems:`, + `maxLength:`, + `maxProperties:`, + `minimum:`, + `minItems:`, + `minLength:`, + `minProperties:`, + `default:`, + }), }, { name: "new crd validation failure for existing field removal", @@ -187,20 +177,31 @@ func TestInstall(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-field-removed.json"), }, - wantErrMsgs: []string{ - `"NoExistingFieldRemoved"`, + requireErr: wantErrorMsgs([]string{ + `existingFieldRemoval:`, + }), + }, + { + name: "new crd validation should not fail on description changes", + // Separate test from above as this error will cause the validator to + // return early and skip some of the above validations. + oldCrdPath: "old-crd.json", + release: &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-description-changed.json"), }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - preflight := newMockPreflight(getCrdFromManifestFile(t, tc.oldCrdPath), tc.wantCrdGetErr, tc.validator) - err := preflight.Install(context.Background(), tc.release) - if len(tc.wantErrMsgs) != 0 { - for _, expectedErrMsg := range tc.wantErrMsgs { - require.ErrorContainsf(t, err, expectedErrMsg, "") - } + preflight := newMockPreflight(getCrdFromManifestFile(t, tc.oldCrdPath), tc.wantCrdGetErr) + objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(tc.release) + if err == nil { + err = preflight.Install(context.Background(), objs) + } + if tc.requireErr != nil { + tc.requireErr(t, err) } else { require.NoError(t, err) } @@ -212,9 +213,8 @@ func TestUpgrade(t *testing.T) { tests := []struct { name string oldCrdPath string - validator *crdupgradesafety.Validator release *release.Release - wantErrMsgs []string + requireErr require.ErrorAssertionFunc wantCrdGetErr error }{ { @@ -232,7 +232,7 @@ func TestUpgrade(t *testing.T) { Name: "test-release", Manifest: "abcd", }, - wantErrMsgs: []string{"json: cannot unmarshal string into Go value of type unstructured.detector"}, + requireErr: wantErrorMsgs([]string{"json: cannot unmarshal string into Go value of type unstructured.detector"}), }, { name: "release with no CRD objects", @@ -248,7 +248,7 @@ func TestUpgrade(t *testing.T) { Manifest: getManifestString(t, "crd-valid-upgrade.json"), }, wantCrdGetErr: fmt.Errorf("error!"), - wantErrMsgs: []string{"error!"}, + requireErr: wantErrorMsgs([]string{"error!"}), }, { name: "fail to get old crd, not found error", @@ -264,23 +264,7 @@ func TestUpgrade(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-invalid"), }, - wantErrMsgs: []string{"json: cannot unmarshal"}, - }, - { - name: "custom validator", - oldCrdPath: "old-crd.json", - release: &release.Release{ - Name: "test-release", - Manifest: getManifestString(t, "old-crd.json"), - }, - validator: &crdupgradesafety.Validator{ - Validations: []crdupgradesafety.Validation{ - crdupgradesafety.NewValidationFunc("test", func(old, new apiextensionsv1.CustomResourceDefinition) error { - return fmt.Errorf("custom validation error!!") - }), - }, - }, - wantErrMsgs: []string{"custom validation error!!"}, + requireErr: wantErrorMsgs([]string{"json: cannot unmarshal"}), }, { name: "valid upgrade", @@ -299,21 +283,21 @@ func TestUpgrade(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-invalid-upgrade.json"), }, - wantErrMsgs: []string{ - `"NoScopeChange"`, - `"NoStoredVersionRemoved"`, - `enum constraints`, - `new required fields`, - `maximum: constraint`, - `maxItems: constraint`, - `maxLength: constraint`, - `maxProperties: constraint`, - `minimum: constraint`, - `minItems: constraint`, - `minLength: constraint`, - `minProperties: constraint`, - `default value`, - }, + requireErr: wantErrorMsgs([]string{ + `scope:`, + `storedVersionRemoval:`, + `enum:`, + `required:`, + `maximum:`, + `maxItems:`, + `maxLength:`, + `maxProperties:`, + `minimum:`, + `minItems:`, + `minLength:`, + `minProperties:`, + `default:`, + }), }, { name: "new crd validation failure for existing field removal", @@ -324,9 +308,9 @@ func TestUpgrade(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-field-removed.json"), }, - wantErrMsgs: []string{ - `"NoExistingFieldRemoved"`, - }, + requireErr: wantErrorMsgs([]string{ + `existingFieldRemoval:`, + }), }, { name: "webhook conversion strategy exists", @@ -343,23 +327,102 @@ func TestUpgrade(t *testing.T) { Name: "test-release", Manifest: getManifestString(t, "crd-conversion-no-webhook.json"), }, - wantErrMsgs: []string{ - `"ServedVersionValidator" validation failed: version upgrade "v1" to "v2", field "^.spec.foobarbaz": enums`, + requireErr: wantErrorMsgs([]string{ + `validating upgrade for CRD "crontabs.stable.example.com": v1 -> v2: ^.spec.foobarbaz: enum: allowed enum values removed`, + }), + }, + { + name: "new crd validation should not fail on description changes", + // Separate test from above as this error will cause the validator to + // return early and skip some of the above validations. + oldCrdPath: "old-crd.json", + release: &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-description-changed.json"), + }, + }, + { + name: "success when old crd and new crd contain the exact same validation issues", + oldCrdPath: "crd-conversion-no-webhook.json", + release: &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-conversion-no-webhook.json"), + }, + }, + { + name: "failure when old crd and new crd contain the exact same validation issues, but new crd introduces another validation issue", + oldCrdPath: "crd-conversion-no-webhook.json", + release: &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-conversion-no-webhook-extra-issue.json"), + }, + requireErr: func(t require.TestingT, err error, _ ...interface{}) { + require.ErrorContains(t, err, + `validating upgrade for CRD "crontabs.stable.example.com":`, + ) + // The newly introduced issue is reported + require.Contains(t, err.Error(), + `v1 -> v2: ^.spec.extraField: type: type changed : "boolean" -> "string"`, + ) + // The existing issue is not reported + require.NotContains(t, err.Error(), + `v1 -> v2: ^.spec.foobarbaz: enum: allowed enum values removed`, + ) }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - preflight := newMockPreflight(getCrdFromManifestFile(t, tc.oldCrdPath), tc.wantCrdGetErr, tc.validator) - err := preflight.Upgrade(context.Background(), tc.release) - if len(tc.wantErrMsgs) != 0 { - for _, expectedErrMsg := range tc.wantErrMsgs { - require.ErrorContainsf(t, err, expectedErrMsg, "") - } + preflight := newMockPreflight(getCrdFromManifestFile(t, tc.oldCrdPath), tc.wantCrdGetErr) + objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(tc.release) + if err == nil { + err = preflight.Upgrade(context.Background(), objs) + } + if tc.requireErr != nil { + tc.requireErr(t, err) } else { require.NoError(t, err) } }) } } + +func TestUpgrade_UnhandledChanges_InSpec_DefaultPolicy(t *testing.T) { + t.Run("unhandled spec changes cause error by default", func(t *testing.T) { + preflight := newMockPreflight(getCrdFromManifestFile(t, "crd-unhandled-old.json"), nil) + rel := &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-unhandled-new.json"), + } + objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(rel) + require.NoError(t, err) + err = preflight.Upgrade(context.Background(), objs) + require.Error(t, err) + require.ErrorContains(t, err, "unhandled changes found") + require.ErrorContains(t, err, "Format \"\" -> \"email\"") + require.NotContains(t, err.Error(), "v1.JSONSchemaProps", "error message should be concise without raw diff") + }) +} + +func TestUpgrade_UnhandledChanges_PolicyError(t *testing.T) { + t.Run("unhandled changes error when policy is Error", func(t *testing.T) { + oldCrd := getCrdFromManifestFile(t, "crd-unhandled-old.json") + preflight := crdupgradesafety.NewPreflight(&MockCRDGetter{oldCrd: oldCrd}, crdupgradesafety.WithConfig(&crdifyconfig.Config{ + Conversion: crdifyconfig.ConversionPolicyIgnore, + UnhandledEnforcement: crdifyconfig.EnforcementPolicyError, + })) + + rel := &release.Release{ + Name: "test-release", + Manifest: getManifestString(t, "crd-unhandled-new.json"), + } + + objs, err := applier.HelmReleaseToObjectsConverter{}.GetObjectsFromRelease(rel) + require.NoError(t, err) + err = preflight.Upgrade(context.Background(), objs) + require.Error(t, err) + require.ErrorContains(t, err, "unhandled changes found") + require.ErrorContains(t, err, "Format \"\" -> \"email\"") + }) +} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator.go deleted file mode 100644 index d66f1ed9c0..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator.go +++ /dev/null @@ -1,74 +0,0 @@ -package crdupgradesafety - -import ( - "errors" - "fmt" - "maps" - "slices" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - versionhelper "k8s.io/apimachinery/pkg/version" -) - -type ServedVersionValidator struct { - Validations []ChangeValidation -} - -func (c *ServedVersionValidator) Validate(old, new apiextensionsv1.CustomResourceDefinition) error { - // If conversion webhook is specified, pass check - if new.Spec.Conversion != nil && new.Spec.Conversion.Strategy == apiextensionsv1.WebhookConverter { - return nil - } - - errs := []error{} - servedVersions := []apiextensionsv1.CustomResourceDefinitionVersion{} - for _, version := range new.Spec.Versions { - if version.Served { - servedVersions = append(servedVersions, version) - } - } - - slices.SortFunc(servedVersions, func(a, b apiextensionsv1.CustomResourceDefinitionVersion) int { - return versionhelper.CompareKubeAwareVersionStrings(a.Name, b.Name) - }) - - for i, oldVersion := range servedVersions[:len(servedVersions)-1] { - for _, newVersion := range servedVersions[i+1:] { - flatOld := FlattenSchema(oldVersion.Schema.OpenAPIV3Schema) - flatNew := FlattenSchema(newVersion.Schema.OpenAPIV3Schema) - diffs, err := CalculateFlatSchemaDiff(flatOld, flatNew) - if err != nil { - errs = append(errs, fmt.Errorf("calculating schema diff between CRD versions %q and %q", oldVersion.Name, newVersion.Name)) - continue - } - - for _, field := range slices.Sorted(maps.Keys(diffs)) { - diff := diffs[field] - - handled := false - for _, validation := range c.Validations { - ok, err := validation(diff) - if err != nil { - errs = append(errs, fmt.Errorf("version upgrade %q to %q, field %q: %w", oldVersion.Name, newVersion.Name, field, err)) - } - if ok { - handled = true - break - } - } - - if !handled { - errs = append(errs, fmt.Errorf("version %q, field %q has unknown change, refusing to determine that change is safe", oldVersion.Name, field)) - } - } - } - } - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -func (c *ServedVersionValidator) Name() string { - return "ServedVersionValidator" -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator_test.go deleted file mode 100644 index 67b0c62055..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/shared_version_validator_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package crdupgradesafety_test - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety" -) - -func TestServedVersionValidator(t *testing.T) { - validationErr1 := errors.New(`version "v1alpha1", field "^" has unknown change, refusing to determine that change is safe`) - validationErr2 := errors.New(`version upgrade "v1alpha1" to "v1alpha2", field "^": fail`) - - for _, tc := range []struct { - name string - servedVersionValidator *crdupgradesafety.ServedVersionValidator - new apiextensionsv1.CustomResourceDefinition - expectedError error - }{ - { - name: "no changes, no error", - servedVersionValidator: &crdupgradesafety.ServedVersionValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("should not run") - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - }, - }, - }, - }, - { - name: "changes, validation successful, change is fully handled, no error", - servedVersionValidator: &crdupgradesafety.ServedVersionValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return true, nil - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - { - Name: "v1alpha2", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - { - name: "changes, validation successful, change not fully handled, error", - servedVersionValidator: &crdupgradesafety.ServedVersionValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, nil - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - { - Name: "v1alpha2", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: validationErr1, - }, - { - name: "changes, validation failed, change fully handled, error", - servedVersionValidator: &crdupgradesafety.ServedVersionValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return true, errors.New("fail") - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - { - Name: "v1alpha2", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: validationErr2, - }, - { - name: "changes, validation failed, change not fully handled, ordered error", - servedVersionValidator: &crdupgradesafety.ServedVersionValidator{ - Validations: []crdupgradesafety.ChangeValidation{ - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("fail") - }, - func(_ crdupgradesafety.FieldDiff) (bool, error) { - return false, errors.New("error") - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{}, - }, - }, - { - Name: "v1alpha2", - Served: true, - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - ID: "foo", - }, - }, - }, - }, - }, - }, - expectedError: fmt.Errorf("%w\n%s\n%w", validationErr2, `version upgrade "v1alpha1" to "v1alpha2", field "^": error`, validationErr1), - }, - } { - t.Run(tc.name, func(t *testing.T) { - err := tc.servedVersionValidator.Validate(apiextensionsv1.CustomResourceDefinition{}, tc.new) - if tc.expectedError != nil { - assert.EqualError(t, err, tc.expectedError.Error()) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-no-webhook-extra-issue.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-no-webhook-extra-issue.json new file mode 100644 index 0000000000..0bfd133843 --- /dev/null +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-no-webhook-extra-issue.json @@ -0,0 +1,76 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "crontabs.stable.example.com" + }, + "spec": { + "group": "stable.example.com", + "versions": [ + { + "name": "v2", + "served": true, + "storage": false, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "foobarbaz": { + "type":"string", + "enum":[ + "bark", + "woof" + ] + }, + "extraField": { + "type":"string" + } + } + } + } + } + } + }, + { + "name": "v1", + "served": true, + "storage": false, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "foobarbaz": { + "type":"string", + "enum":[ + "foo", + "bar", + "baz" + ] + }, + "extraField": { + "type":"boolean" + } + } + } + } + } + } + } + ], + "scope": "Cluster", + "names": { + "plural": "crontabs", + "singular": "crontab", + "kind": "CronTab", + "shortNames": [ + "ct" + ] + } + } +} diff --git a/testdata/manifests/crd-conversion-no-webhook.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-no-webhook.json similarity index 100% rename from testdata/manifests/crd-conversion-no-webhook.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-no-webhook.json diff --git a/testdata/manifests/crd-conversion-webhook-old.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-webhook-old.json similarity index 100% rename from testdata/manifests/crd-conversion-webhook-old.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-webhook-old.json diff --git a/testdata/manifests/crd-conversion-webhook.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-webhook.json similarity index 100% rename from testdata/manifests/crd-conversion-webhook.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-conversion-webhook.json diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-description-changed.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-description-changed.json new file mode 100644 index 0000000000..0e7f9a600b --- /dev/null +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-description-changed.json @@ -0,0 +1,124 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "crontabs.stable.example.com" + }, + "spec": { + "group": "stable.example.com", + "versions": [ + { + "name": "v1", + "served": true, + "storage": false, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "description": "description two", + "type": "object", + "properties": { + "removedField": { + "type":"integer" + }, + "enum": { + "type": "string", + "enum": ["a", "b", "c"] + }, + "minMaxValue": { + "type":"integer" + }, + "required": { + "type":"integer" + }, + "minMaxItems": { + "type":"array", + "items": { + "type":"string" + } + }, + "minMaxLength": { + "type":"string" + }, + "defaultVal": { + "type": "string" + }, + "requiredVal": { + "type": "string" + } + } + } + }, + "required": [ + "requiredVal" + ] + } + } + }, + { + "name": "v2", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "removedField": { + "type":"integer" + }, + "enum": { + "type": "string", + "enum": ["a", "b", "c"] + }, + "minMaxValue": { + "type":"integer" + }, + "required": { + "type":"integer" + }, + "minMaxItems": { + "type":"array", + "items": { + "type":"string" + } + }, + "minMaxLength": { + "type":"string" + }, + "defaultVal": { + "type": "string" + }, + "requiredVal": { + "type": "string" + } + } + } + }, + "required": [ + "requiredVal" + ] + } + } + } + ], + "scope": "Cluster", + "names": { + "plural": "crontabs", + "singular": "crontab", + "kind": "CronTab", + "shortNames": [ + "ct" + ] + } + }, + "status": { + "storedVersions": [ + "v1", + "v2" + ] + } +} diff --git a/testdata/manifests/crd-field-removed.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-field-removed.json similarity index 96% rename from testdata/manifests/crd-field-removed.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-field-removed.json index 86ba06e409..650b13fd48 100644 --- a/testdata/manifests/crd-field-removed.json +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-field-removed.json @@ -22,7 +22,8 @@ "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c"] }, "minMaxValue": { "type":"integer" @@ -66,7 +67,8 @@ "type": "object", "properties": { "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c"] }, "minMaxValue": { "type":"integer" diff --git a/testdata/manifests/crd-invalid b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-invalid similarity index 100% rename from testdata/manifests/crd-invalid rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-invalid diff --git a/testdata/manifests/crd-invalid-upgrade.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-invalid-upgrade.json similarity index 92% rename from testdata/manifests/crd-invalid-upgrade.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-invalid-upgrade.json index 4131a68fbf..3c95ccb25a 100644 --- a/testdata/manifests/crd-invalid-upgrade.json +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-invalid-upgrade.json @@ -24,11 +24,8 @@ "type":"integer" }, "enum": { - "type":"integer", - "enum":[ - 1, - 2 - ] + "type": "string", + "enum": ["a", "b"] }, "minMaxValue": { "type":"integer", diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-new.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-new.json new file mode 100644 index 0000000000..6fed77fc16 --- /dev/null +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-new.json @@ -0,0 +1,40 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "widgets.example.com" + }, + "spec": { + "group": "example.com", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "field": { + "type": "string", + "format": "email" + } + } + } + } + } + } + } + ], + "scope": "Namespaced", + "names": { + "plural": "widgets", + "singular": "widget", + "kind": "Widget" + } + } +} + diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-old.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-old.json new file mode 100644 index 0000000000..a87fbd5054 --- /dev/null +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-unhandled-old.json @@ -0,0 +1,39 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "widgets.example.com" + }, + "spec": { + "group": "example.com", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "field": { + "type": "string" + } + } + } + } + } + } + } + ], + "scope": "Namespaced", + "names": { + "plural": "widgets", + "singular": "widget", + "kind": "Widget" + } + } +} + diff --git a/testdata/manifests/crd-valid-upgrade.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-valid-upgrade.json similarity index 93% rename from testdata/manifests/crd-valid-upgrade.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-valid-upgrade.json index 52380dc92b..cbc2e3ec14 100644 --- a/testdata/manifests/crd-valid-upgrade.json +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/crd-valid-upgrade.json @@ -22,7 +22,8 @@ "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c", "adding-enum-is-allowed"] }, "minMaxValue": { "type":"integer" @@ -69,7 +70,8 @@ "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c", "adding-enum-is-allowed"] }, "minMaxValue": { "type":"integer" @@ -116,7 +118,8 @@ "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c", "adding-enum-is-allowed"] }, "minMaxValue": { "type":"integer" diff --git a/testdata/manifests/no-crds.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/no-crds.json similarity index 100% rename from testdata/manifests/no-crds.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/no-crds.json diff --git a/testdata/manifests/old-crd.json b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/old-crd.json similarity index 93% rename from testdata/manifests/old-crd.json rename to internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/old-crd.json index 08e7634518..5a8c55b321 100644 --- a/testdata/manifests/old-crd.json +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/testdata/manifests/old-crd.json @@ -16,13 +16,15 @@ "type": "object", "properties": { "spec": { + "description": "description one", "type": "object", "properties": { "removedField": { "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c"] }, "minMaxValue": { "type":"integer" @@ -69,7 +71,8 @@ "type":"integer" }, "enum": { - "type":"integer" + "type": "string", + "enum": ["a", "b", "c"] }, "minMaxValue": { "type":"integer" diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/unhandled_message_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/unhandled_message_test.go new file mode 100644 index 0000000000..59078655a0 --- /dev/null +++ b/internal/operator-controller/rukpak/preflights/crdupgradesafety/unhandled_message_test.go @@ -0,0 +1,28 @@ +package crdupgradesafety + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConciseUnhandledMessage_NoPrefix(t *testing.T) { + raw := "some other error" + require.Equal(t, raw, conciseUnhandledMessage(raw)) +} + +func TestConciseUnhandledMessage_SingleChange(t *testing.T) { + raw := "unhandled changes found :\n- Format: \"\"\n+ Format: \"email\"\n" + require.Equal(t, "unhandled changes found (Format \"\" -> \"email\")", conciseUnhandledMessage(raw)) +} + +func TestConciseUnhandledMessage_MultipleChanges(t *testing.T) { + raw := "unhandled changes found :\n- Format: \"\"\n+ Format: \"email\"\n- Default: nil\n+ Default: \"value\"\n- Title: \"\"\n+ Title: \"Widget\"\n- Description: \"old\"\n+ Description: \"new\"\n" + got := conciseUnhandledMessage(raw) + require.Equal(t, "unhandled changes found (Format \"\" -> \"email\"; Default nil -> \"value\"; Title \"\" -> \"Widget\"; Description \"old\" -> \"new\")", got) +} + +func TestConciseUnhandledMessage_SkipComplexValues(t *testing.T) { + raw := "unhandled changes found :\n- Default: &v1.JSONSchemaProps{}\n+ Default: &v1.JSONSchemaProps{Type: \"string\"}\n" + require.Equal(t, "unhandled changes found (Default -> (changed))", conciseUnhandledMessage(raw)) +} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator.go deleted file mode 100644 index 6fec6cbe5a..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator.go +++ /dev/null @@ -1,123 +0,0 @@ -// Originally copied from https://github.com/carvel-dev/kapp/tree/d7fc2e15439331aa3a379485bb124e91a0829d2e -// Attribution: -// Copyright 2024 The Carvel Authors. -// SPDX-License-Identifier: Apache-2.0 - -package crdupgradesafety - -import ( - "errors" - "fmt" - "strings" - - "github.com/openshift/crd-schema-checker/pkg/manifestcomparators" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/util/sets" -) - -// Validation is a representation of a validation to run -// against a CRD being upgraded -type Validation interface { - // Validate contains the actual validation logic. An error being - // returned means validation has failed - Validate(old, new apiextensionsv1.CustomResourceDefinition) error - // Name returns a human-readable name for the validation - Name() string -} - -// ValidateFunc is a function to validate a CustomResourceDefinition -// for safe upgrades. It accepts the old and new CRDs and returns an -// error if performing an upgrade from old -> new is unsafe. -type ValidateFunc func(old, new apiextensionsv1.CustomResourceDefinition) error - -// ValidationFunc is a helper to wrap a ValidateFunc -// as an implementation of the Validation interface -type ValidationFunc struct { - name string - validateFunc ValidateFunc -} - -func NewValidationFunc(name string, vfunc ValidateFunc) Validation { - return &ValidationFunc{ - name: name, - validateFunc: vfunc, - } -} - -func (vf *ValidationFunc) Name() string { - return vf.name -} - -func (vf *ValidationFunc) Validate(old, new apiextensionsv1.CustomResourceDefinition) error { - return vf.validateFunc(old, new) -} - -type Validator struct { - Validations []Validation -} - -func (v *Validator) Validate(old, new apiextensionsv1.CustomResourceDefinition) error { - validateErrs := []error{} - for _, validation := range v.Validations { - if err := validation.Validate(old, new); err != nil { - formattedErr := fmt.Errorf("CustomResourceDefinition %s failed upgrade safety validation. %q validation failed: %w", - new.Name, validation.Name(), err) - - validateErrs = append(validateErrs, formattedErr) - } - } - if len(validateErrs) > 0 { - return errors.Join(validateErrs...) - } - return nil -} - -func NoScopeChange(old, new apiextensionsv1.CustomResourceDefinition) error { - if old.Spec.Scope != new.Spec.Scope { - return fmt.Errorf("scope changed from %q to %q", old.Spec.Scope, new.Spec.Scope) - } - return nil -} - -func NoStoredVersionRemoved(old, new apiextensionsv1.CustomResourceDefinition) error { - newVersions := sets.New[string]() - for _, version := range new.Spec.Versions { - if !newVersions.Has(version.Name) { - newVersions.Insert(version.Name) - } - } - - for _, storedVersion := range old.Status.StoredVersions { - if !newVersions.Has(storedVersion) { - return fmt.Errorf("stored version %q removed", storedVersion) - } - } - - return nil -} - -func NoExistingFieldRemoved(old, new apiextensionsv1.CustomResourceDefinition) error { - reg := manifestcomparators.NewRegistry() - err := reg.AddComparator(manifestcomparators.NoFieldRemoval()) - if err != nil { - return err - } - - results, errs := reg.Compare(&old, &new) - if len(errs) > 0 { - return errors.Join(errs...) - } - - errSet := []error{} - - for _, result := range results { - if len(result.Errors) > 0 { - errSet = append(errSet, errors.New(strings.Join(result.Errors, "\n"))) - } - } - if len(errSet) > 0 { - return errors.Join(errSet...) - } - - return nil -} diff --git a/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator_test.go b/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator_test.go deleted file mode 100644 index e13ac94877..0000000000 --- a/internal/operator-controller/rukpak/preflights/crdupgradesafety/validator_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Originally copied from https://github.com/carvel-dev/kapp/tree/d7fc2e15439331aa3a379485bb124e91a0829d2e -// Attribution: -// Copyright 2024 The Carvel Authors. -// SPDX-License-Identifier: Apache-2.0 - -package crdupgradesafety - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" -) - -func TestValidator(t *testing.T) { - for _, tc := range []struct { - name string - validations []Validation - shouldErr bool - }{ - { - name: "no validators, no error", - validations: []Validation{}, - }, - { - name: "passing validator, no error", - validations: []Validation{ - NewValidationFunc("pass", func(_, _ apiextensionsv1.CustomResourceDefinition) error { - return nil - }), - }, - }, - { - name: "failing validator, error", - validations: []Validation{ - NewValidationFunc("fail", func(_, _ apiextensionsv1.CustomResourceDefinition) error { - return errors.New("boom") - }), - }, - shouldErr: true, - }, - { - name: "passing+failing validator, error", - validations: []Validation{ - NewValidationFunc("pass", func(_, _ apiextensionsv1.CustomResourceDefinition) error { - return nil - }), - NewValidationFunc("fail", func(_, _ apiextensionsv1.CustomResourceDefinition) error { - return errors.New("boom") - }), - }, - shouldErr: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - v := Validator{ - Validations: tc.validations, - } - var o, n apiextensionsv1.CustomResourceDefinition - - err := v.Validate(o, n) - require.Equal(t, tc.shouldErr, err != nil) - }) - } -} - -func TestNoScopeChange(t *testing.T) { - for _, tc := range []struct { - name string - old apiextensionsv1.CustomResourceDefinition - new apiextensionsv1.CustomResourceDefinition - shouldError bool - }{ - { - name: "no scope change, no error", - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.ClusterScoped, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.ClusterScoped, - }, - }, - }, - { - name: "scope change, error", - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.ClusterScoped, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.NamespaceScoped, - }, - }, - shouldError: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - err := NoScopeChange(tc.old, tc.new) - require.Equal(t, tc.shouldError, err != nil) - }) - } -} - -func TestNoStoredVersionRemoved(t *testing.T) { - for _, tc := range []struct { - name string - old apiextensionsv1.CustomResourceDefinition - new apiextensionsv1.CustomResourceDefinition - shouldError bool - }{ - { - name: "no stored versions, no error", - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - }, - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{}, - }, - { - name: "stored versions, no stored version removed, no error", - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - }, - { - Name: "v1alpha2", - }, - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Status: apiextensionsv1.CustomResourceDefinitionStatus{ - StoredVersions: []string{ - "v1alpha1", - }, - }, - }, - }, - { - name: "stored versions, stored version removed, error", - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha2", - }, - }, - }, - }, - old: apiextensionsv1.CustomResourceDefinition{ - Status: apiextensionsv1.CustomResourceDefinitionStatus{ - StoredVersions: []string{ - "v1alpha1", - }, - }, - }, - shouldError: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - err := NoStoredVersionRemoved(tc.old, tc.new) - require.Equal(t, tc.shouldError, err != nil) - }) - } -} - -func TestNoExistingFieldRemoved(t *testing.T) { - for _, tc := range []struct { - name string - new apiextensionsv1.CustomResourceDefinition - old apiextensionsv1.CustomResourceDefinition - shouldError bool - }{ - { - name: "no existing field removed, no error", - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "existing field removed, error", - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - "fieldTwo": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - shouldError: true, - }, - { - name: "new version is added with the field removed, no error", - old: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - "fieldTwo": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - new: apiextensionsv1.CustomResourceDefinition{ - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - "fieldTwo": { - Type: "string", - }, - }, - }, - }, - }, - { - Name: "v1alpha2", - Schema: &apiextensionsv1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensionsv1.JSONSchemaProps{ - "fieldOne": { - Type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - err := NoExistingFieldRemoved(tc.old, tc.new) - assert.Equal(t, tc.shouldError, err != nil) - }) - } -} diff --git a/internal/operator-controller/rukpak/render/certprovider.go b/internal/operator-controller/rukpak/render/certprovider.go index f3920a4c74..80c9e67ad3 100644 --- a/internal/operator-controller/rukpak/render/certprovider.go +++ b/internal/operator-controller/rukpak/render/certprovider.go @@ -28,10 +28,10 @@ type CertSecretInfo struct { // CertificateProvisionerConfig contains the necessary information for a CertificateProvider // to correctly generate and modify object for certificate injection and automation type CertificateProvisionerConfig struct { - WebhookServiceName string - CertName string - Namespace string - CertProvider CertificateProvider + ServiceName string + CertName string + Namespace string + CertProvider CertificateProvider } // CertificateProvisioner uses a CertificateProvider to modify and generate objects based on its @@ -70,9 +70,9 @@ func CertProvisionerFor(deploymentName string, opts Options) CertificateProvisio certName := util.ObjectNameForBaseAndSuffix(webhookServiceName, "cert") return CertificateProvisioner{ - CertProvider: opts.CertificateProvider, - WebhookServiceName: webhookServiceName, - Namespace: opts.InstallNamespace, - CertName: certName, + CertProvider: opts.CertificateProvider, + ServiceName: webhookServiceName, + Namespace: opts.InstallNamespace, + CertName: certName, } } diff --git a/internal/operator-controller/rukpak/render/certprovider_test.go b/internal/operator-controller/rukpak/render/certprovider_test.go index 3005cfd73f..a245a4173a 100644 --- a/internal/operator-controller/rukpak/render/certprovider_test.go +++ b/internal/operator-controller/rukpak/render/certprovider_test.go @@ -16,10 +16,10 @@ import ( func Test_CertificateProvisioner_WithoutCertProvider(t *testing.T) { provisioner := &render.CertificateProvisioner{ - WebhookServiceName: "webhook", - CertName: "cert", - Namespace: "namespace", - CertProvider: nil, + ServiceName: "webhook", + CertName: "cert", + Namespace: "namespace", + CertProvider: nil, } require.NoError(t, provisioner.InjectCABundle(&corev1.Secret{})) @@ -50,10 +50,10 @@ func Test_CertificateProvisioner_WithCertProvider(t *testing.T) { }, } provisioner := &render.CertificateProvisioner{ - WebhookServiceName: "webhook", - CertName: "cert", - Namespace: "namespace", - CertProvider: fakeProvider, + ServiceName: "webhook", + CertName: "cert", + Namespace: "namespace", + CertProvider: fakeProvider, } svc := &corev1.Service{} @@ -83,10 +83,10 @@ func Test_CertificateProvisioner_Errors(t *testing.T) { }, } provisioner := &render.CertificateProvisioner{ - WebhookServiceName: "webhook", - CertName: "cert", - Namespace: "namespace", - CertProvider: fakeProvider, + ServiceName: "webhook", + CertName: "cert", + Namespace: "namespace", + CertProvider: fakeProvider, } err := provisioner.InjectCABundle(&corev1.Service{}) @@ -107,7 +107,7 @@ func Test_CertProvisionerFor(t *testing.T) { }) require.Equal(t, prov.CertProvider, fakeProvider) - require.Equal(t, "my-deployment-thing-service", prov.WebhookServiceName) + require.Equal(t, "my-deployment-thing-service", prov.ServiceName) require.Equal(t, "my-deployment-thing-service-cert", prov.CertName) require.Equal(t, "my-namespace", prov.Namespace) } @@ -115,8 +115,8 @@ func Test_CertProvisionerFor(t *testing.T) { func Test_CertProvisionerFor_ExtraLargeName_MoreThan63Chars(t *testing.T) { prov := render.CertProvisionerFor("my.object.thing.has.a.really.really.really.really.really.long.name", render.Options{}) - require.Len(t, prov.WebhookServiceName, 63) + require.Len(t, prov.ServiceName, 63) require.Len(t, prov.CertName, 63) - require.Equal(t, "my-object-thing-has-a-really-really-really-really-reall-service", prov.WebhookServiceName) + require.Equal(t, "my-object-thing-has-a-really-really-really-really-reall-service", prov.ServiceName) require.Equal(t, "my-object-thing-has-a-really-really-really-really-reall-se-cert", prov.CertName) } diff --git a/internal/operator-controller/rukpak/render/certproviders/certmanager.go b/internal/operator-controller/rukpak/render/certproviders/certmanager.go index f9dada3f00..0cde052c7f 100644 --- a/internal/operator-controller/rukpak/render/certproviders/certmanager.go +++ b/internal/operator-controller/rukpak/render/certproviders/certmanager.go @@ -20,6 +20,7 @@ import ( const ( certManagerInjectCAAnnotation = "cert-manager.io/inject-ca-from" olmv0RotationPeriod = 730 * 24 * time.Hour // 2 year rotation + olmv0RenewBefore = 24 * time.Hour // renew certificate within 24h of expiry ) var _ render.CertificateProvider = (*CertManagerCertificateProvider)(nil) @@ -55,6 +56,7 @@ func (p CertManagerCertificateProvider) AdditionalObjects(cfg render.Certificate // OLMv0 parity: // - self-signed issuer // - 2 year rotation period + // - renew 24h before expiry // - CN: argocd-operator-controller-manager-service.argocd (-service.) // - CA: false // - DNS:argocd-operator-controller-manager-service.argocd, DNS:argocd-operator-controller-manager-service.argocd.svc, DNS:argocd-operator-controller-manager-service.argocd.svc.cluster.local @@ -151,13 +153,13 @@ func (p CertManagerCertificateProvider) AdditionalObjects(cfg render.Certificate }, Spec: certmanagerv1.CertificateSpec{ SecretName: cfg.CertName, - CommonName: fmt.Sprintf("%s.%s", cfg.WebhookServiceName, cfg.Namespace), + CommonName: fmt.Sprintf("%s.%s", cfg.ServiceName, cfg.Namespace), Usages: []certmanagerv1.KeyUsage{certmanagerv1.UsageServerAuth}, IsCA: false, DNSNames: []string{ - fmt.Sprintf("%s.%s", cfg.WebhookServiceName, cfg.Namespace), - fmt.Sprintf("%s.%s.svc", cfg.WebhookServiceName, cfg.Namespace), - fmt.Sprintf("%s.%s.svc.cluster.local", cfg.WebhookServiceName, cfg.Namespace), + fmt.Sprintf("%s.%s", cfg.ServiceName, cfg.Namespace), + fmt.Sprintf("%s.%s.svc", cfg.ServiceName, cfg.Namespace), + fmt.Sprintf("%s.%s.svc.cluster.local", cfg.ServiceName, cfg.Namespace), }, IssuerRef: certmanagermetav1.ObjectReference{ Name: issuer.GetName(), @@ -165,6 +167,9 @@ func (p CertManagerCertificateProvider) AdditionalObjects(cfg render.Certificate Duration: &metav1.Duration{ Duration: olmv0RotationPeriod, }, + RenewBefore: &metav1.Duration{ + Duration: olmv0RenewBefore, + }, }, } certObj, err := util.ToUnstructured(certificate) diff --git a/internal/operator-controller/rukpak/render/certproviders/certmanager_test.go b/internal/operator-controller/rukpak/render/certproviders/certmanager_test.go index b5da581d3a..2acb40d729 100644 --- a/internal/operator-controller/rukpak/render/certproviders/certmanager_test.go +++ b/internal/operator-controller/rukpak/render/certproviders/certmanager_test.go @@ -30,9 +30,9 @@ func Test_CertManagerProvider_InjectCABundle(t *testing.T) { name: "injects certificate annotation in validating webhook configuration", obj: &admissionregistrationv1.ValidatingWebhookConfiguration{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ @@ -46,9 +46,9 @@ func Test_CertManagerProvider_InjectCABundle(t *testing.T) { name: "injects certificate annotation in mutating webhook configuration", obj: &admissionregistrationv1.MutatingWebhookConfiguration{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ @@ -62,9 +62,9 @@ func Test_CertManagerProvider_InjectCABundle(t *testing.T) { name: "injects certificate annotation in custom resource definition", obj: &apiextensionsv1.CustomResourceDefinition{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ @@ -78,9 +78,9 @@ func Test_CertManagerProvider_InjectCABundle(t *testing.T) { name: "ignores other objects", obj: &corev1.Service{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &corev1.Service{}, }, @@ -96,9 +96,9 @@ func Test_CertManagerProvider_InjectCABundle(t *testing.T) { func Test_CertManagerProvider_AdditionalObjects(t *testing.T) { certProvier := certproviders.CertManagerCertificateProvider{} objs, err := certProvier.AdditionalObjects(render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }) require.NoError(t, err) require.Equal(t, []unstructured.Unstructured{ @@ -143,6 +143,10 @@ func Test_CertManagerProvider_AdditionalObjects(t *testing.T) { // OLMv0 has a 2 year certificate rotation period Duration: 730 * 24 * time.Hour, }, + RenewBefore: &metav1.Duration{ + // OLMv0 reviews 24h before expiry + Duration: 24 * time.Hour, + }, }, }), }, objs) @@ -151,9 +155,9 @@ func Test_CertManagerProvider_AdditionalObjects(t *testing.T) { func Test_CertManagerProvider_GetCertSecretInfo(t *testing.T) { certProvier := certproviders.CertManagerCertificateProvider{} certInfo := certProvier.GetCertSecretInfo(render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }) require.Equal(t, render.CertSecretInfo{ SecretName: "cert-name", diff --git a/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go index 24e8ecc126..c4ca3e525c 100644 --- a/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go +++ b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go @@ -25,9 +25,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { name: "injects inject-cabundle annotation in validating webhook configuration", obj: &admissionregistrationv1.ValidatingWebhookConfiguration{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &admissionregistrationv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ @@ -41,9 +41,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { name: "injects inject-cabundle annotation in mutating webhook configuration", obj: &admissionregistrationv1.MutatingWebhookConfiguration{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &admissionregistrationv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ @@ -57,9 +57,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { name: "injects inject-cabundle annotation in custom resource definition", obj: &apiextensionsv1.CustomResourceDefinition{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ @@ -73,9 +73,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { name: "injects serving-cert-secret-name annotation in service resource referencing the certificate name", obj: &corev1.Service{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -89,9 +89,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { name: "ignores other objects", obj: &corev1.Secret{}, cfg: render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }, expectedObj: &corev1.Secret{}, }, @@ -107,9 +107,9 @@ func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) { func Test_OpenshiftServiceCAProvider_AdditionalObjects(t *testing.T) { certProvider := certproviders.OpenshiftServiceCaCertificateProvider{} objs, err := certProvider.AdditionalObjects(render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }) require.NoError(t, err) require.Nil(t, objs) @@ -118,9 +118,9 @@ func Test_OpenshiftServiceCAProvider_AdditionalObjects(t *testing.T) { func Test_OpenshiftServiceCAProvider_GetCertSecretInfo(t *testing.T) { certProvider := certproviders.OpenshiftServiceCaCertificateProvider{} certInfo := certProvider.GetCertSecretInfo(render.CertificateProvisionerConfig{ - WebhookServiceName: "webhook-service", - Namespace: "namespace", - CertName: "cert-name", + ServiceName: "webhook-service", + Namespace: "namespace", + CertName: "cert-name", }) require.Equal(t, render.CertSecretInfo{ SecretName: "cert-name", diff --git a/internal/operator-controller/rukpak/render/fake.go b/internal/operator-controller/rukpak/render/fake.go new file mode 100644 index 0000000000..c8213d78a8 --- /dev/null +++ b/internal/operator-controller/rukpak/render/fake.go @@ -0,0 +1,24 @@ +package render + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FakeCertProvider struct { + InjectCABundleFn func(obj client.Object, cfg CertificateProvisionerConfig) error + AdditionalObjectsFn func(cfg CertificateProvisionerConfig) ([]unstructured.Unstructured, error) + GetCertSecretInfoFn func(cfg CertificateProvisionerConfig) CertSecretInfo +} + +func (f FakeCertProvider) InjectCABundle(obj client.Object, cfg CertificateProvisionerConfig) error { + return f.InjectCABundleFn(obj, cfg) +} + +func (f FakeCertProvider) AdditionalObjects(cfg CertificateProvisionerConfig) ([]unstructured.Unstructured, error) { + return f.AdditionalObjectsFn(cfg) +} + +func (f FakeCertProvider) GetCertSecretInfo(cfg CertificateProvisionerConfig) CertSecretInfo { + return f.GetCertSecretInfoFn(cfg) +} diff --git a/internal/operator-controller/rukpak/render/registryv1/generators/generators.go b/internal/operator-controller/rukpak/render/registryv1/generators/generators.go index cf17142fa1..7d5d435ead 100644 --- a/internal/operator-controller/rukpak/render/registryv1/generators/generators.go +++ b/internal/operator-controller/rukpak/render/registryv1/generators/generators.go @@ -3,7 +3,6 @@ package generators import ( "cmp" "fmt" - "maps" "slices" "strconv" "strings" @@ -13,6 +12,7 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" @@ -26,14 +26,31 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" ) -var certVolumeMounts = map[string]corev1.VolumeMount{ - "apiservice-cert": { - Name: "apiservice-cert", - MountPath: "/apiserver.local.config/certificates", - }, - "webhook-cert": { - Name: "webhook-cert", - MountPath: "/tmp/k8s-webhook-server/serving-certs", +const ( + labelKubernetesNamespaceMetadataName = "kubernetes.io/metadata.name" +) + +type certVolumeConfig struct { + Name string + Path string + TLSCertPath string + TLSKeyPath string +} + +// certVolumeConfigs contain the expected configurations for certificate volume/mounts +// that the generated Deployment resources for bundle containing webhooks and/or apiservices +// should contain. +var certVolumeConfigs = []certVolumeConfig{ + { + Name: "webhook-cert", + Path: "/tmp/k8s-webhook-server/serving-certs", + TLSCertPath: "tls.crt", + TLSKeyPath: "tls.key", + }, { + Name: "apiservice-cert", + Path: "/apiserver.local.config/certificates", + TLSCertPath: "apiserver.crt", + TLSKeyPath: "apiserver.key", }, } @@ -78,7 +95,7 @@ func BundleCSVDeploymentGenerator(rv1 *bundle.RegistryV1, opts render.Options) ( secretInfo := render.CertProvisionerFor(depSpec.Name, opts).GetCertSecretInfo() if webhookDeployments.Has(depSpec.Name) && secretInfo != nil { - addCertVolumesToDeployment(deploymentResource, *secretInfo) + ensureCorrectDeploymentCertVolumes(deploymentResource, *secretInfo) } objs = append(objs, deploymentResource) @@ -105,10 +122,7 @@ func BundleCSVPermissionsGenerator(rv1 *bundle.RegistryV1, opts render.Options) for _, ns := range opts.TargetNamespaces { for _, permission := range permissions { saName := saNameOrDefault(permission.ServiceAccountName) - name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) - if err != nil { - return nil, err - } + name := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) objs = append(objs, CreateRoleResource(name, ns, WithRules(permission.Rules...)), @@ -150,10 +164,7 @@ func BundleCSVClusterPermissionsGenerator(rv1 *bundle.RegistryV1, opts render.Op objs := make([]client.Object, 0, 2*len(clusterPermissions)) for _, permission := range clusterPermissions { saName := saNameOrDefault(permission.ServiceAccountName) - name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) - if err != nil { - return nil, err - } + name := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) objs = append(objs, CreateClusterRoleResource(name, WithRules(permission.Rules...)), CreateClusterRoleBindingResource( @@ -241,7 +252,7 @@ func BundleCRDGenerator(rv1 *bundle.RegistryV1, opts render.Options) ([]client.O ClientConfig: &apiextensionsv1.WebhookClientConfig{ Service: &apiextensionsv1.ServiceReference{ Namespace: opts.InstallNamespace, - Name: certProvisioner.WebhookServiceName, + Name: certProvisioner.ServiceName, Path: &conversionWebhookPath, Port: &cw.ContainerPort, }, @@ -292,6 +303,7 @@ func BundleValidatingWebhookResourceGenerator(rv1 *bundle.RegistryV1, opts rende //nolint:prealloc var objs []client.Object + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { if wh.Type != v1alpha1.ValidatingAdmissionWebhook { continue @@ -314,11 +326,14 @@ func BundleValidatingWebhookResourceGenerator(rv1 *bundle.RegistryV1, opts rende ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Namespace: opts.InstallNamespace, - Name: certProvisioner.WebhookServiceName, + Name: certProvisioner.ServiceName, Path: wh.WebhookPath, Port: &wh.ContainerPort, }, }, + // It is safe to create a namespace selector even for cluster scoped CRs. A webhook + // is never skipped for cluster scoped CRs. + NamespaceSelector: getWebhookNamespaceSelector(opts.TargetNamespaces), }, ), ) @@ -362,12 +377,15 @@ func BundleMutatingWebhookResourceGenerator(rv1 *bundle.RegistryV1, opts render. ClientConfig: admissionregistrationv1.WebhookClientConfig{ Service: &admissionregistrationv1.ServiceReference{ Namespace: opts.InstallNamespace, - Name: certProvisioner.WebhookServiceName, + Name: certProvisioner.ServiceName, Path: wh.WebhookPath, Port: &wh.ContainerPort, }, }, ReinvocationPolicy: wh.ReinvocationPolicy, + // It is safe to create a namespace selector even for cluster scoped CRs. A webhook + // is never skipped for cluster scoped CRs. + NamespaceSelector: getWebhookNamespaceSelector(opts.TargetNamespaces), }, ), ) @@ -379,10 +397,10 @@ func BundleMutatingWebhookResourceGenerator(rv1 *bundle.RegistryV1, opts render. return objs, nil } -// BundleWebhookServiceResourceGenerator generates Service resources based that support the webhooks defined in -// the bundle's cluster service version spec. The resource is modified by the CertificateProvider in opts +// BundleDeploymentServiceResourceGenerator generates Service resources that support, e.g. the webhooks, +// defined in the bundle's cluster service version spec. The resource is modified by the CertificateProvider in opts // to add any annotations or modifications necessary for certificate injection. -func BundleWebhookServiceResourceGenerator(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { +func BundleDeploymentServiceResourceGenerator(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { if rv1 == nil { return nil, fmt.Errorf("bundle cannot be nil") } @@ -415,7 +433,7 @@ func BundleWebhookServiceResourceGenerator(rv1 *bundle.RegistryV1, opts render.O certProvisioner := render.CertProvisionerFor(deploymentSpec.Name, opts) serviceResource := CreateServiceResource( - certProvisioner.WebhookServiceName, + certProvisioner.ServiceName, opts.InstallNamespace, WithServiceSpec( corev1.ServiceSpec{ @@ -479,65 +497,84 @@ func getWebhookServicePort(wh v1alpha1.WebhookDescription) corev1.ServicePort { } } -func addCertVolumesToDeployment(dep *appsv1.Deployment, certSecretInfo render.CertSecretInfo) { - // update pod volumes - dep.Spec.Template.Spec.Volumes = slices.Concat( - slices.DeleteFunc(dep.Spec.Template.Spec.Volumes, func(v corev1.Volume) bool { - _, ok := certVolumeMounts[v.Name] - return ok - }), - []corev1.Volume{ - { - Name: "apiservice-cert", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: certSecretInfo.SecretName, - Items: []corev1.KeyToPath{ - { - Key: certSecretInfo.CertificateKey, - Path: "apiserver.crt", - }, - { - Key: certSecretInfo.PrivateKeyKey, - Path: "apiserver.key", - }, +// ensureCorrectDeploymentCertVolumes ensures the deployment has the correct certificate volume mounts by +// - removing all existing volumes with protected certificate volume names (i.e. webhook-cert and apiservice-cert) +// - removing all existing volumes that point to the protected certificate paths (e.g. /tmp/k8s-webhook-server/serving-certs) +// - adding the correct certificate volumes with the correct configuration +// - applying the same changes to all container volume mounts +func ensureCorrectDeploymentCertVolumes(dep *appsv1.Deployment, certSecretInfo render.CertSecretInfo) { + // collect volumes and paths to replace + volumesToRemove := sets.New[string]() + protectedVolumePaths := sets.New[string]() + certVolumes := make([]corev1.Volume, 0, len(certVolumeConfigs)) + certVolumeMounts := make([]corev1.VolumeMount, 0, len(certVolumeConfigs)) + for _, cfg := range certVolumeConfigs { + volumesToRemove.Insert(cfg.Name) + protectedVolumePaths.Insert(cfg.Path) + certVolumes = append(certVolumes, corev1.Volume{ + Name: cfg.Name, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: certSecretInfo.SecretName, + Items: []corev1.KeyToPath{ + { + Key: certSecretInfo.CertificateKey, + Path: cfg.TLSCertPath, }, - }, - }, - }, { - Name: "webhook-cert", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: certSecretInfo.SecretName, - Items: []corev1.KeyToPath{ - { - Key: certSecretInfo.CertificateKey, - Path: "tls.crt", - }, - { - Key: certSecretInfo.PrivateKeyKey, - Path: "tls.key", - }, + { + Key: certSecretInfo.PrivateKeyKey, + Path: cfg.TLSKeyPath, }, }, }, }, - }, + }) + certVolumeMounts = append(certVolumeMounts, corev1.VolumeMount{ + Name: cfg.Name, + MountPath: cfg.Path, + }) + } + + for _, c := range dep.Spec.Template.Spec.Containers { + for _, containerVolumeMount := range c.VolumeMounts { + if protectedVolumePaths.Has(containerVolumeMount.MountPath) { + volumesToRemove.Insert(containerVolumeMount.Name) + } + } + } + + // update pod volumes + dep.Spec.Template.Spec.Volumes = slices.Concat( + slices.DeleteFunc(dep.Spec.Template.Spec.Volumes, func(v corev1.Volume) bool { + return volumesToRemove.Has(v.Name) + }), + certVolumes, ) // update container volume mounts for i := range dep.Spec.Template.Spec.Containers { dep.Spec.Template.Spec.Containers[i].VolumeMounts = slices.Concat( slices.DeleteFunc(dep.Spec.Template.Spec.Containers[i].VolumeMounts, func(v corev1.VolumeMount) bool { - _, ok := certVolumeMounts[v.Name] - return ok + return volumesToRemove.Has(v.Name) }), - slices.SortedFunc( - maps.Values(certVolumeMounts), - func(a corev1.VolumeMount, b corev1.VolumeMount) int { - return cmp.Compare(a.Name, b.Name) - }, - ), + certVolumeMounts, ) } } + +// getWebhookNamespaceSelector returns a label selector that matches any namespace in targetNamespaces. +// If targetNamespaces is empty, nil, or includes "" (signifying all namespaces) nil is returned. +func getWebhookNamespaceSelector(targetNamespaces []string) *metav1.LabelSelector { + if len(targetNamespaces) > 0 && !slices.Contains(targetNamespaces, "") { + return &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: labelKubernetesNamespaceMetadataName, + Operator: metav1.LabelSelectorOpIn, + Values: targetNamespaces, + }, + }, + } + } + return nil +} diff --git a/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go b/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go index 91d02de029..59be3c6df1 100644 --- a/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go +++ b/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go @@ -24,6 +24,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1/generators" . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) func Test_ResourceGenerators(t *testing.T) { @@ -67,10 +68,10 @@ func Test_BundleCSVDeploymentGenerator_Succeeds(t *testing.T) { { name: "generates deployment resources", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithAnnotations(map[string]string{ "csv": "annotation", - }), + }). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "deployment-one", @@ -94,8 +95,7 @@ func Test_BundleCSVDeploymentGenerator_Succeeds(t *testing.T) { Name: "deployment-two", Spec: appsv1.DeploymentSpec{}, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -173,13 +173,13 @@ func Test_BundleCSVDeploymentGenerator_WithCertWithCertProvider_Succeeds(t *test }, } - bundle := &bundle.RegistryV1{ - CSV: MakeCSV( + b := &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, DeploymentName: "deployment-one", - }), + }). // deployment must have a referencing webhook (or owned apiservice) definition to trigger cert secret WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ @@ -188,32 +188,59 @@ func Test_BundleCSVDeploymentGenerator_WithCertWithCertProvider_Succeeds(t *test Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ + // volume that have neither protected names: webhook-cert and apiservice-cert, + // or target protected certificate paths should remain untouched { - Name: "apiservice-cert", + Name: "some-other-mount", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + // volume mounts with protected names will be rewritten to ensure they point to + // the right certificate path. If they do not exist, they will be created. { - Name: "some-other-mount", + Name: "webhook-cert", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + // volumes that point to protected paths will be removed + { + Name: "some-mount", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "some-webhook-cert-mount", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, - // expect webhook-cert volume to be injected }, Containers: []corev1.Container{ { Name: "container-1", VolumeMounts: []corev1.VolumeMount{ - // expect apiservice-cert volume to be injected + // the mount path for the following volume will be replaced + // since the volume name is protected { Name: "webhook-cert", MountPath: "/webhook-cert-path", - }, { + }, + // the following volume will be preserved + { Name: "some-other-mount", MountPath: "/some/other/mount/path", }, + // these volume mount will be removed for referencing protected cert paths + { + Name: "some-webhook-cert-mount", + MountPath: "/tmp/k8s-webhook-server/serving-certs", + }, { + Name: "some-mount", + MountPath: "/apiserver.local.config/certificates", + }, }, }, { @@ -225,11 +252,10 @@ func Test_BundleCSVDeploymentGenerator_WithCertWithCertProvider_Succeeds(t *test }, }, }, - ), - ), + ).Build(), } - objs, err := generators.BundleCSVDeploymentGenerator(bundle, render.Options{ + objs, err := generators.BundleCSVDeploymentGenerator(b, render.Options{ InstallNamespace: "install-namespace", CertificateProvider: fakeProvider, }) @@ -247,35 +273,36 @@ func Test_BundleCSVDeploymentGenerator_WithCertWithCertProvider_Succeeds(t *test }, }, { - Name: "apiservice-cert", + Name: "webhook-cert", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: "some-secret", Items: []corev1.KeyToPath{ { Key: "some-cert-key", - Path: "apiserver.crt", + Path: "tls.crt", }, { Key: "some-private-key-key", - Path: "apiserver.key", + Path: "tls.key", }, }, }, }, - }, { - Name: "webhook-cert", + }, + { + Name: "apiservice-cert", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: "some-secret", Items: []corev1.KeyToPath{ { Key: "some-cert-key", - Path: "tls.crt", + Path: "apiserver.crt", }, { Key: "some-private-key-key", - Path: "tls.key", + Path: "apiserver.key", }, }, }, @@ -290,27 +317,27 @@ func Test_BundleCSVDeploymentGenerator_WithCertWithCertProvider_Succeeds(t *test Name: "some-other-mount", MountPath: "/some/other/mount/path", }, - { - Name: "apiservice-cert", - MountPath: "/apiserver.local.config/certificates", - }, { Name: "webhook-cert", MountPath: "/tmp/k8s-webhook-server/serving-certs", }, + { + Name: "apiservice-cert", + MountPath: "/apiserver.local.config/certificates", + }, }, }, { Name: "container-2", VolumeMounts: []corev1.VolumeMount{ - { - Name: "apiservice-cert", - MountPath: "/apiserver.local.config/certificates", - }, { Name: "webhook-cert", MountPath: "/tmp/k8s-webhook-server/serving-certs", }, + { + Name: "apiservice-cert", + MountPath: "/apiserver.local.config/certificates", + }, }, }, }, deployment.Spec.Template.Spec.Containers) @@ -324,8 +351,8 @@ func Test_BundleCSVDeploymentGenerator_FailsOnNil(t *testing.T) { } func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { - fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) { - return base, nil + fakeUniqueNameGenerator := func(base string, _ interface{}) string { + return base } for _, tc := range []struct { @@ -342,8 +369,8 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -355,8 +382,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: nil, }, @@ -368,8 +394,8 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -385,8 +411,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.Role{ @@ -443,8 +468,8 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -460,8 +485,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.Role{ @@ -562,8 +586,8 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -585,8 +609,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.Role{ @@ -679,8 +702,8 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "", @@ -692,8 +715,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.Role{ @@ -745,7 +767,7 @@ func Test_BundleCSVPermissionsGenerator_Succeeds(t *testing.T) { for i := range objs { require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) } - require.Equal(t, len(tc.expectedResources), len(objs)) + require.Len(t, objs, len(tc.expectedResources)) }) } } @@ -758,8 +780,8 @@ func Test_BundleCSVPermissionGenerator_FailsOnNil(t *testing.T) { } func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { - fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) { - return base, nil + fakeUniqueNameGenerator := func(base string, _ interface{}) string { + return base } for _, tc := range []struct { @@ -776,8 +798,8 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -799,8 +821,7 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.ClusterRole{ @@ -897,8 +918,8 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithClusterPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-one", @@ -920,8 +941,7 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.ClusterRole{ @@ -1010,8 +1030,8 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { UniqueNameGenerator: fakeUniqueNameGenerator, }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithClusterPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "", @@ -1023,8 +1043,7 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &rbacv1.ClusterRole{ @@ -1074,7 +1093,7 @@ func Test_BundleCSVClusterPermissionsGenerator_Succeeds(t *testing.T) { for i := range objs { require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) } - require.Equal(t, len(tc.expectedResources), len(objs)) + require.Len(t, objs, len(tc.expectedResources)) }) } } @@ -1099,8 +1118,8 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { InstallNamespace: "install-namespace", }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-1", @@ -1122,7 +1141,7 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { }, }, }, - ), + ). WithClusterPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "service-account-2", @@ -1144,8 +1163,7 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: []client.Object{ &corev1.ServiceAccount{ @@ -1186,8 +1204,8 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { InstallNamespace: "install-namespace", }, bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithName("csv"), + CSV: clusterserviceversion.Builder(). + WithName("csv"). WithPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "", @@ -1199,7 +1217,7 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { }, }, }, - ), + ). WithClusterPermissions( v1alpha1.StrategyDeploymentPermissions{ ServiceAccountName: "", @@ -1211,8 +1229,7 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { }, }, }, - ), - ), + ).Build(), }, expectedResources: nil, }, @@ -1226,7 +1243,7 @@ func Test_BundleCSVServiceAccountGenerator_Succeeds(t *testing.T) { for i := range objs { require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) } - require.Equal(t, len(tc.expectedResources), len(objs)) + require.Len(t, objs, len(tc.expectedResources)) }) } } @@ -1270,7 +1287,7 @@ func Test_BundleCRDGenerator_WithConversionWebhook_Succeeds(t *testing.T) { {ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, {ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, }, - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -1288,8 +1305,7 @@ func Test_BundleCRDGenerator_WithConversionWebhook_Succeeds(t *testing.T) { ConversionCRDs: []string{"crd-two"}, DeploymentName: "some-deployment", }, - ), - ), + ).Build(), } objs, err := generators.BundleCRDGenerator(bundle, opts) @@ -1355,7 +1371,7 @@ func Test_BundleCRDGenerator_WithConversionWebhook_Fails(t *testing.T) { }, }, }, - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -1365,8 +1381,7 @@ func Test_BundleCRDGenerator_WithConversionWebhook_Fails(t *testing.T) { ConversionCRDs: []string{"crd-one"}, DeploymentName: "some-deployment", }, - ), - ), + ).Build(), } objs, err := generators.BundleCRDGenerator(bundle, opts) @@ -1396,7 +1411,7 @@ func Test_BundleCRDGenerator_WithCertProvider_Succeeds(t *testing.T) { {ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, {ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, }, - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -1405,8 +1420,7 @@ func Test_BundleCRDGenerator_WithCertProvider_Succeeds(t *testing.T) { "crd-one", }, }, - ), - ), + ).Build(), } objs, err := generators.BundleCRDGenerator(bundle, opts) @@ -1486,7 +1500,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "generates validating webhook configuration resources described in the bundle's cluster service version", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -1519,12 +1533,11 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { WebhookPath: ptr.To("/webhook-path"), ContainerPort: 443, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", - TargetNamespaces: []string{"watch-namespace-one", "watch-namespace-two"}, + TargetNamespaces: []string{""}, }, expectedResources: []client.Object{ &admissionregistrationv1.ValidatingWebhookConfiguration{ @@ -1571,6 +1584,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + // No NamespaceSelector is set targetNamespaces = []string{""} (AllNamespaces install mode) }, }, }, @@ -1579,7 +1593,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "removes any - suffixes from the webhook name (v0 used GenerateName to allow multiple operator installations - we don't want that in v1)", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -1612,8 +1626,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { WebhookPath: ptr.To("/webhook-path"), ContainerPort: 443, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -1664,6 +1677,15 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"watch-namespace-one", "watch-namespace-two"}, + }, + }, + }, }, }, }, @@ -1672,7 +1694,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "generates validating webhook configuration resources with certificate provider modifications", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -1680,8 +1702,7 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { DeploymentName: "my-deployment", ContainerPort: 443, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -1711,6 +1732,15 @@ func Test_BundleValidatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"watch-namespace-one", "watch-namespace-two"}, + }, + }, + }, }, }, }, @@ -1750,7 +1780,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "generates validating webhook configuration resources described in the bundle's cluster service version", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -1784,12 +1814,11 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { ContainerPort: 443, ReinvocationPolicy: ptr.To(admissionregistrationv1.IfNeededReinvocationPolicy), }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", - TargetNamespaces: []string{"watch-namespace-one", "watch-namespace-two"}, + TargetNamespaces: []string{""}, }, expectedResources: []client.Object{ &admissionregistrationv1.MutatingWebhookConfiguration{ @@ -1837,6 +1866,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + // No NamespaceSelector is set targetNamespaces = []string{""} (AllNamespaces install mode) }, }, }, @@ -1845,7 +1875,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "removes any - suffixes from the webhook name (v0 used GenerateName to allow multiple operator installations - we don't want that in v1)", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -1879,8 +1909,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { ContainerPort: 443, ReinvocationPolicy: ptr.To(admissionregistrationv1.IfNeededReinvocationPolicy), }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -1932,6 +1961,15 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"watch-namespace-one", "watch-namespace-two"}, + }, + }, + }, }, }, }, @@ -1940,7 +1978,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { { name: "generates validating webhook configuration resources with certificate provider modifications", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -1948,8 +1986,7 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { DeploymentName: "my-deployment", ContainerPort: 443, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -1979,6 +2016,15 @@ func Test_BundleMutatingWebhookResourceGenerator_Succeeds(t *testing.T) { Port: ptr.To(int32(443)), }, }, + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"watch-namespace-one", "watch-namespace-two"}, + }, + }, + }, }, }, }, @@ -2000,7 +2046,7 @@ func Test_BundleMutatingWebhookResourceGenerator_FailsOnNil(t *testing.T) { require.Contains(t, err.Error(), "bundle cannot be nil") } -func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { +func Test_BundleDeploymentServiceResourceGenerator_Succeeds(t *testing.T) { fakeProvider := FakeCertProvider{ InjectCABundleFn: func(obj client.Object, cfg render.CertificateProvisionerConfig) error { obj.SetAnnotations(map[string]string{ @@ -2018,18 +2064,17 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "generates webhook services using container port 443 and target port 443 by default", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, DeploymentName: "my-deployment", }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2063,19 +2108,18 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "generates webhook services using the given container port and setting target port the same as the container port if not given", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, DeploymentName: "my-deployment", ContainerPort: int32(8443), }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2109,11 +2153,11 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "generates webhook services using given container port of 443 and given target port", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -2123,8 +2167,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { IntVal: 8080, }, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2158,11 +2201,11 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "generates webhook services using given container port and target port", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -2173,8 +2216,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { IntVal: 9099, }, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2208,7 +2250,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "generates webhook services using referenced deployment defined label selector", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", @@ -2219,7 +2261,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { }, }, }, - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -2230,8 +2272,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { IntVal: 9099, }, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2268,7 +2309,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "aggregates all webhook definitions referencing the same deployment into a single service", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", @@ -2279,7 +2320,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { }, }, }, - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -2307,8 +2348,7 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { IntVal: 9099, }, }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2366,18 +2406,17 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { { name: "applies cert provider modifiers to webhook service", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", - }), + }). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, DeploymentName: "my-deployment", }, - ), - ), + ).Build(), }, opts: render.Options{ InstallNamespace: "install-namespace", @@ -2414,14 +2453,14 @@ func Test_BundleWebhookServiceResourceGenerator_Succeeds(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - objs, err := generators.BundleWebhookServiceResourceGenerator(tc.bundle, tc.opts) + objs, err := generators.BundleDeploymentServiceResourceGenerator(tc.bundle, tc.opts) require.NoError(t, err) require.Equal(t, tc.expectedResources, objs) }) } } -func Test_BundleWebhookServiceResourceGenerator_FailsOnNil(t *testing.T) { +func Test_BundleDeploymentServiceResourceGenerator_FailsOnNil(t *testing.T) { objs, err := generators.BundleMutatingWebhookResourceGenerator(nil, render.Options{}) require.Nil(t, objs) require.Error(t, err) @@ -2441,14 +2480,14 @@ func Test_CertProviderResourceGenerator_Succeeds(t *testing.T) { } objs, err := generators.CertProviderResourceGenerator(&bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( // only generate resources for deployments referenced by webhook definitions v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, DeploymentName: "my-deployment", }, - ), + ). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{ Name: "my-deployment", @@ -2456,8 +2495,7 @@ func Test_CertProviderResourceGenerator_Succeeds(t *testing.T) { v1alpha1.StrategyDeploymentSpec{ Name: "my-other-deployment", }, - ), - ), + ).Build(), }, render.Options{ InstallNamespace: "install-namespace", CertificateProvider: fakeProvider, diff --git a/internal/operator-controller/rukpak/render/registryv1/registryv1.go b/internal/operator-controller/rukpak/render/registryv1/registryv1.go index 61f0e6ef08..1cfefbb8be 100644 --- a/internal/operator-controller/rukpak/render/registryv1/registryv1.go +++ b/internal/operator-controller/rukpak/render/registryv1/registryv1.go @@ -22,11 +22,13 @@ var BundleValidator = render.BundleValidator{ validators.CheckCRDResourceUniqueness, validators.CheckOwnedCRDExistence, validators.CheckPackageNameNotEmpty, + validators.CheckConversionWebhookSupport, validators.CheckWebhookDeploymentReferentialIntegrity, validators.CheckWebhookNameUniqueness, validators.CheckWebhookNameIsDNS1123SubDomain, validators.CheckConversionWebhookCRDReferenceUniqueness, validators.CheckConversionWebhooksReferenceOwnedCRDs, + validators.CheckWebhookRules, } // ResourceGenerators a slice of ResourceGenerators required to generate plain resource manifests for @@ -43,6 +45,6 @@ var ResourceGenerators = []render.ResourceGenerator{ generators.BundleCSVDeploymentGenerator, generators.BundleValidatingWebhookResourceGenerator, generators.BundleMutatingWebhookResourceGenerator, - generators.BundleWebhookServiceResourceGenerator, + generators.BundleDeploymentServiceResourceGenerator, generators.CertProviderResourceGenerator, } diff --git a/internal/operator-controller/rukpak/render/registryv1/registryv1_test.go b/internal/operator-controller/rukpak/render/registryv1/registryv1_test.go index ed1b3294f3..b092cc8e11 100644 --- a/internal/operator-controller/rukpak/render/registryv1/registryv1_test.go +++ b/internal/operator-controller/rukpak/render/registryv1/registryv1_test.go @@ -17,6 +17,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1/generators" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1/validators" . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) func Test_BundleValidatorHasAllValidationFns(t *testing.T) { @@ -26,15 +27,17 @@ func Test_BundleValidatorHasAllValidationFns(t *testing.T) { validators.CheckCRDResourceUniqueness, validators.CheckOwnedCRDExistence, validators.CheckPackageNameNotEmpty, + validators.CheckConversionWebhookSupport, validators.CheckWebhookDeploymentReferentialIntegrity, validators.CheckWebhookNameUniqueness, validators.CheckWebhookNameIsDNS1123SubDomain, validators.CheckConversionWebhookCRDReferenceUniqueness, validators.CheckConversionWebhooksReferenceOwnedCRDs, + validators.CheckWebhookRules, } actualValidationFns := registryv1.BundleValidator - require.Equal(t, len(expectedValidationFns), len(actualValidationFns)) + require.Len(t, actualValidationFns, len(expectedValidationFns)) for i := range expectedValidationFns { require.Equal(t, reflect.ValueOf(expectedValidationFns[i]).Pointer(), reflect.ValueOf(actualValidationFns[i]).Pointer(), "bundle validator has unexpected validation function") } @@ -50,24 +53,23 @@ func Test_ResourceGeneratorsHasAllGenerators(t *testing.T) { generators.BundleCSVDeploymentGenerator, generators.BundleValidatingWebhookResourceGenerator, generators.BundleMutatingWebhookResourceGenerator, - generators.BundleWebhookServiceResourceGenerator, + generators.BundleDeploymentServiceResourceGenerator, generators.CertProviderResourceGenerator, } actualGenerators := registryv1.ResourceGenerators - require.Equal(t, len(expectedGenerators), len(actualGenerators)) + require.Len(t, actualGenerators, len(expectedGenerators)) for i := range expectedGenerators { require.Equal(t, reflect.ValueOf(expectedGenerators[i]).Pointer(), reflect.ValueOf(actualGenerators[i]).Pointer(), "bundle validator has unexpected validation function") } } func Test_Renderer_Success(t *testing.T) { - bundle := bundle.RegistryV1{ + someBundle := bundle.RegistryV1{ PackageName: "my-package", - CSV: MakeCSV( - WithName("test-bundle"), - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), - ), + CSV: clusterserviceversion.Builder(). + WithName("test-bundle"). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), Others: []unstructured.Unstructured{ *ToUnstructuredT(t, &corev1.Service{ TypeMeta: metav1.TypeMeta{ @@ -81,7 +83,7 @@ func Test_Renderer_Success(t *testing.T) { }, } - objs, err := registryv1.Renderer.Render(bundle, "install-namespace") + objs, err := registryv1.Renderer.Render(someBundle, "install-namespace") t.Log("Check renderer returns objects and no errors") require.NoError(t, err) require.NotEmpty(t, objs) @@ -96,12 +98,11 @@ func Test_Renderer_Success(t *testing.T) { } func Test_Renderer_Failure_UnsupportedKind(t *testing.T) { - bundle := bundle.RegistryV1{ + someBundle := bundle.RegistryV1{ PackageName: "my-package", - CSV: MakeCSV( - WithName("test-bundle"), - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), - ), + CSV: clusterserviceversion.Builder(). + WithName("test-bundle"). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), Others: []unstructured.Unstructured{ *ToUnstructuredT(t, &corev1.Event{ TypeMeta: metav1.TypeMeta{ @@ -115,7 +116,7 @@ func Test_Renderer_Failure_UnsupportedKind(t *testing.T) { }, } - objs, err := registryv1.Renderer.Render(bundle, "install-namespace") + objs, err := registryv1.Renderer.Render(someBundle, "install-namespace") t.Log("Check renderer returns objects and no errors") require.Error(t, err) require.Contains(t, err.Error(), "unsupported resource") diff --git a/internal/operator-controller/rukpak/render/registryv1/validators/validator.go b/internal/operator-controller/rukpak/render/registryv1/validators/validator.go index 61d0aad7cd..60978aa833 100644 --- a/internal/operator-controller/rukpak/render/registryv1/validators/validator.go +++ b/internal/operator-controller/rukpak/render/registryv1/validators/validator.go @@ -101,21 +101,32 @@ func CheckPackageNameNotEmpty(rv1 *bundle.RegistryV1) []error { return nil } -// CheckWebhookSupport checks that if the bundle cluster service version declares webhook definitions -// that it is a singleton operator, i.e. that it only supports AllNamespaces mode. This keeps parity -// with OLMv0 behavior for conversion webhooks, +// CheckConversionWebhookSupport checks that if the bundle cluster service version declares conversion webhook definitions, +// that the bundle also only supports AllNamespaces install mode. This keeps parity with OLMv0 behavior for conversion webhooks, // https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/install/webhook.go#L193 -// Since OLMv1 considers APIs to be cluster-scoped, we initially extend this constraint to validating and mutating webhooks. -// While this might restrict the number of supported bundles, we can tackle the issue of relaxing this constraint in turn -// after getting the webhook support working. -func CheckWebhookSupport(rv1 *bundle.RegistryV1) []error { - if len(rv1.CSV.Spec.WebhookDefinitions) > 0 { +func CheckConversionWebhookSupport(rv1 *bundle.RegistryV1) []error { + var conversionWebhookNames []string + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + if wh.Type == v1alpha1.ConversionWebhook { + conversionWebhookNames = append(conversionWebhookNames, wh.GenerateName) + } + } + + if len(conversionWebhookNames) > 0 { supportedInstallModes := sets.Set[v1alpha1.InstallModeType]{} for _, mode := range rv1.CSV.Spec.InstallModes { - supportedInstallModes.Insert(mode.Type) + if mode.Supported { + supportedInstallModes.Insert(mode.Type) + } } + if len(supportedInstallModes) != 1 || !supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces) { - return []error{errors.New("bundle contains webhook definitions but supported install modes beyond AllNamespaces")} + sortedModes := slices.Sorted(slices.Values(supportedInstallModes.UnsortedList())) + errs := make([]error, len(conversionWebhookNames)) + for i, webhookName := range conversionWebhookNames { + errs[i] = fmt.Errorf("bundle contains conversion webhook %q and supports install modes %v - conversion webhooks are only supported for bundles that only support AllNamespaces install mode", webhookName, sortedModes) + } + return errs } } @@ -264,3 +275,55 @@ func CheckWebhookNameIsDNS1123SubDomain(rv1 *bundle.RegistryV1) []error { } return errs } + +// forbiddenWebhookRuleAPIGroups contain the API groups that are forbidden for webhook configuration rules in OLMv1 +var forbiddenWebhookRuleAPIGroups = sets.New("olm.operatorframework.io", "*") + +// forbiddenAdmissionRegistrationResources contain the resources that are forbidden for webhook configuration rules +// for the admissionregistration.k8s.io api group +var forbiddenAdmissionRegistrationResources = sets.New( + "*", + "mutatingwebhookconfiguration", + "mutatingwebhookconfigurations", + "validatingwebhookconfiguration", + "validatingwebhookconfigurations", +) + +// CheckWebhookRules ensures webhook rules do not reference forbidden API groups or resources in line with OLMv0 behavior +// The following are forbidden, rules targeting: +// - all API groups (i.e. '*') +// - OLMv1 API group (i.e. 'olm.operatorframework.io') +// - all resources under the 'admissionregistration.k8s.io' API group +// - the 'ValidatingWebhookConfiguration' resource under the 'admissionregistration.k8s.io' API group +// - the 'MutatingWebhookConfiguration' resource under the 'admissionregistration.k8s.io' API group +// +// These boundaries attempt to reduce the blast radius of faulty webhooks and avoid deadlocks preventing the user +// from deleting OLMv1 resources installing and managing the faulty webhook, or deleting faulty admission webhook +// configurations. +// See https://github.com/operator-framework/operator-lifecycle-manager/blob/ccf0c4c91f1e7673e87f3a18947f9a1f88d48438/pkg/controller/install/webhook.go#L19 +// for more details +func CheckWebhookRules(rv1 *bundle.RegistryV1) []error { + var errs []error + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + // Rules are not used for conversion webhooks + if wh.Type == v1alpha1.ConversionWebhook { + continue + } + webhookName := wh.GenerateName + for _, rule := range wh.Rules { + for _, apiGroup := range rule.APIGroups { + if forbiddenWebhookRuleAPIGroups.Has(apiGroup) { + errs = append(errs, fmt.Errorf("webhook %q contains forbidden rule: admission webhook rules cannot reference API group %q", webhookName, apiGroup)) + } + if apiGroup == "admissionregistration.k8s.io" { + for _, resource := range rule.Resources { + if forbiddenAdmissionRegistrationResources.Has(strings.ToLower(resource)) { + errs = append(errs, fmt.Errorf("webhook %q contains forbidden rule: admission webhook rules cannot reference resource %q for API group %q", webhookName, resource, apiGroup)) + } + } + } + } + } + } + return errs +} diff --git a/internal/operator-controller/rukpak/render/registryv1/validators/validator_test.go b/internal/operator-controller/rukpak/render/registryv1/validators/validator_test.go index 6c1d7491b9..b9377c81a4 100644 --- a/internal/operator-controller/rukpak/render/registryv1/validators/validator_test.go +++ b/internal/operator-controller/rukpak/render/registryv1/validators/validator_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -12,7 +13,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1/validators" - . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) func Test_CheckDeploymentSpecUniqueness(t *testing.T) { @@ -24,24 +25,22 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) { { name: "accepts bundles with unique deployment strategy spec names", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"}, - ), - ), + ).Build(), }, expectedErrs: []error{}, }, { name: "rejects bundles with duplicate deployment strategy spec names", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("cluster service version contains duplicate strategy deployment spec 'test-deployment-one'"), @@ -49,15 +48,14 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) { }, { name: "errors are ordered by deployment strategy spec name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-a"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-b"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-c"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-b"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-a"}, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("cluster service version contains duplicate strategy deployment spec 'test-deployment-a'"), @@ -81,24 +79,22 @@ func Test_CheckDeploymentNameIsDNS1123SubDomain(t *testing.T) { { name: "accepts valid deployment strategy spec names", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"}, - ), - ), + ).Build(), }, expectedErrs: []error{}, }, { name: "rejects bundles with invalid deployment strategy spec names - errors are sorted by name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "-bad-name"}, v1alpha1.StrategyDeploymentSpec{Name: "b-name-is-waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay-too-long"}, v1alpha1.StrategyDeploymentSpec{Name: "a-name-is-waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay-too-long-and-bad-"}, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("invalid cluster service version strategy deployment name '-bad-name': a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), @@ -172,21 +168,19 @@ func Test_CheckOwnedCRDExistence(t *testing.T) { {ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}}, {ObjectMeta: metav1.ObjectMeta{Name: "b.crd.something"}}, }, - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "a.crd.something"}, v1alpha1.CRDDescription{Name: "b.crd.something"}, - ), - ), + ).Build(), }, expectedErrs: []error{}, }, { name: "rejects bundles with missing owned custom resource definition resources", bundle: &bundle.RegistryV1{ CRDs: []apiextensionsv1.CustomResourceDefinition{}, - CSV: MakeCSV( - WithOwnedCRDs(v1alpha1.CRDDescription{Name: "a.crd.something"}), - ), + CSV: clusterserviceversion.Builder(). + WithOwnedCRDs(v1alpha1.CRDDescription{Name: "a.crd.something"}).Build(), }, expectedErrs: []error{ errors.New("cluster service definition references owned custom resource definition 'a.crd.something' not found in bundle"), @@ -195,13 +189,12 @@ func Test_CheckOwnedCRDExistence(t *testing.T) { name: "errors are ordered by owned custom resource definition name", bundle: &bundle.RegistryV1{ CRDs: []apiextensionsv1.CustomResourceDefinition{}, - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "a.crd.something"}, v1alpha1.CRDDescription{Name: "c.crd.something"}, v1alpha1.CRDDescription{Name: "b.crd.something"}, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("cluster service definition references owned custom resource definition 'a.crd.something' not found in bundle"), @@ -250,89 +243,363 @@ func Test_CheckWebhookSupport(t *testing.T) { expectedErrs []error }{ { - name: "accepts bundles with validating webhook definitions when they only support AllNamespaces install mode", + name: "accepts bundles with conversion webhook definitions when they only support AllNamespaces install mode", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ConversionWebhook, + }, + ).Build(), + }, + }, + { + name: "accepts bundles with validating webhook definitions when they support more modes than AllNamespaces install mode", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, }, - ), - ), + ).Build(), }, }, { - name: "accepts bundles with mutating webhook definitions when they only support AllNamespaces install mode", + name: "accepts bundles with mutating webhook definitions when they support more modes than AllNamespaces install mode", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, }, - ), - ), + ).Build(), }, }, { - name: "accepts bundles with conversion webhook definitions when they only support AllNamespaces install mode", + name: "rejects bundles with conversion webhook definitions when they support more modes than AllNamespaces install mode", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeAllNamespaces). WithWebhookDefinitions( v1alpha1.WebhookDescription{ - Type: v1alpha1.ConversionWebhook, + GenerateName: "webhook-b", + Type: v1alpha1.ConversionWebhook, }, - ), - ), + v1alpha1.WebhookDescription{ + GenerateName: "webhook-a", + Type: v1alpha1.ConversionWebhook, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("bundle contains conversion webhook \"webhook-b\" and supports install modes [AllNamespaces SingleNamespace] - conversion webhooks are only supported for bundles that only support AllNamespaces install mode"), + errors.New("bundle contains conversion webhook \"webhook-a\" and supports install modes [AllNamespaces SingleNamespace] - conversion webhooks are only supported for bundles that only support AllNamespaces install mode"), }, }, + } { + t.Run(tc.name, func(t *testing.T) { + errs := validators.CheckConversionWebhookSupport(tc.bundle) + require.Equal(t, tc.expectedErrs, errs) + }) + } +} + +func Test_CheckWebhookRules(t *testing.T) { + for _, tc := range []struct { + name string + bundle *bundle.RegistryV1 + expectedErrs []error + }{ { - name: "rejects bundles with validating webhook definitions when they support more modes than AllNamespaces install mode", + name: "accepts bundles with webhook definitions without rules", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, }, - ), - ), + v1alpha1.WebhookDescription{ + Type: v1alpha1.MutatingAdmissionWebhook, + }, + ).Build(), }, - expectedErrs: []error{errors.New("bundle contains webhook definitions but supported install modes beyond AllNamespaces")}, }, { - name: "accepts bundles with mutating webhook definitions when they support more modes than AllNamespaces install mode", + name: "accepts bundles with webhook definitions with supported rules", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments", "replicasets", "statefulsets"}, + }, + }, + }, + }, v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + Resources: []string{"services"}, + }, + }, + }, }, - ), - ), + ).Build(), }, - expectedErrs: []error{errors.New("bundle contains webhook definitions but supported install modes beyond AllNamespaces")}, }, { - name: "accepts bundles with conversion webhook definitions when they support more modes than AllNamespaces install mode", + name: "reject bundles with webhook definitions with rules containing '*' api group", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). WithWebhookDefinitions( v1alpha1.WebhookDescription{ - Type: v1alpha1.ConversionWebhook, + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-z", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"*"}, + }, + }, + }, + }, + v1alpha1.WebhookDescription{ + Type: v1alpha1.MutatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"*"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-z\" contains forbidden rule: admission webhook rules cannot reference API group \"*\""), + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference API group \"*\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'olm.operatorframework.io' api group", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-z", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"olm.operatorframework.io"}, + }, + }, + }, + }, + v1alpha1.WebhookDescription{ + Type: v1alpha1.MutatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"olm.operatorframework.io"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-z\" contains forbidden rule: admission webhook rules cannot reference API group \"olm.operatorframework.io\""), + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference API group \"olm.operatorframework.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and '*' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"*"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"*\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'MutatingWebhookConfiguration' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"MutatingWebhookConfiguration"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"MutatingWebhookConfiguration\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'mutatingwebhookconfiguration' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"mutatingwebhookconfiguration"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"mutatingwebhookconfiguration\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'mutatingwebhookconfigurations' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"mutatingwebhookconfigurations"}, + }, + }, + }, }, - ), - ), + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"mutatingwebhookconfigurations\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'ValidatingWebhookConfiguration' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"ValidatingWebhookConfiguration"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"ValidatingWebhookConfiguration\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'validatingwebhookconfiguration' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"validatingwebhookconfiguration"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"validatingwebhookconfiguration\" for API group \"admissionregistration.k8s.io\""), + }, + }, + { + name: "reject bundles with webhook definitions with rules containing 'admissionregistration.k8s.io' api group and 'validatingwebhookconfigurations' resource", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithWebhookDefinitions( + v1alpha1.WebhookDescription{ + Type: v1alpha1.ValidatingAdmissionWebhook, + GenerateName: "webhook-a", + Rules: []admissionregistrationv1.RuleWithOperations{ + { + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"admissionregistration.k8s.io"}, + Resources: []string{"validatingwebhookconfigurations"}, + }, + }, + }, + }, + ).Build(), + }, + expectedErrs: []error{ + errors.New("webhook \"webhook-a\" contains forbidden rule: admission webhook rules cannot reference resource \"validatingwebhookconfigurations\" for API group \"admissionregistration.k8s.io\""), }, - expectedErrs: []error{errors.New("bundle contains webhook definitions but supported install modes beyond AllNamespaces")}, }, } { t.Run(tc.name, func(t *testing.T) { - errs := validators.CheckWebhookSupport(tc.bundle) + errs := validators.CheckWebhookRules(tc.bundle) require.Equal(t, tc.expectedErrs, errs) }) } @@ -347,35 +614,33 @@ func Test_CheckWebhookDeploymentReferentialIntegrity(t *testing.T) { { name: "accepts bundles where webhook definitions reference existing strategy deployment specs", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, GenerateName: "test-webhook", DeploymentName: "test-deployment-one", }, - ), - ), + ).Build(), }, }, { name: "rejects bundles with webhook definitions that reference non-existing strategy deployment specs", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, GenerateName: "test-webhook", DeploymentName: "test-deployment-two", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("webhook of type 'ValidatingAdmissionWebhook' with name 'test-webhook' references non-existent deployment 'test-deployment-two'"), @@ -383,10 +648,10 @@ func Test_CheckWebhookDeploymentReferentialIntegrity(t *testing.T) { }, { name: "errors are ordered by deployment strategy spec name, webhook type, and webhook name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithStrategyDeploymentSpecs( v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -416,8 +681,7 @@ func Test_CheckWebhookDeploymentReferentialIntegrity(t *testing.T) { GenerateName: "test-conv-webhook-c-a", DeploymentName: "test-deployment-c", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("webhook of type 'MutatingAdmissionWebhook' with name 'test-mute-webhook-a' references non-existent deployment 'test-deployment-a'"), @@ -445,12 +709,12 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { { name: "accepts bundles without webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV(), + CSV: clusterserviceversion.Builder().Build(), }, }, { name: "accepts bundles with unique webhook names", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -471,13 +735,12 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.ConversionWebhook, GenerateName: "test-webhook-six", }, - ), - ), + ).Build(), }, }, { name: "accepts bundles with webhooks with the same name but different types", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -489,13 +752,12 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.ConversionWebhook, GenerateName: "test-webhook", }, - ), - ), + ).Build(), }, }, { name: "rejects bundles with duplicate validating webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -504,8 +766,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.ValidatingAdmissionWebhook, GenerateName: "test-webhook", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("duplicate webhook 'test-webhook' of type 'ValidatingAdmissionWebhook'"), @@ -513,7 +774,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { }, { name: "rejects bundles with duplicate mutating webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.MutatingAdmissionWebhook, @@ -522,8 +783,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.MutatingAdmissionWebhook, GenerateName: "test-webhook", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("duplicate webhook 'test-webhook' of type 'MutatingAdmissionWebhook'"), @@ -531,7 +791,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { }, { name: "rejects bundles with duplicate conversion webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -540,8 +800,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.ConversionWebhook, GenerateName: "test-webhook", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("duplicate webhook 'test-webhook' of type 'ConversionWebhook'"), @@ -549,7 +808,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { }, { name: "orders errors by webhook type and name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -592,8 +851,7 @@ func Test_CheckWebhookNameUniqueness(t *testing.T) { Type: v1alpha1.MutatingAdmissionWebhook, GenerateName: "test-mute-webhook-b", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("duplicate webhook 'test-conv-webhook-a' of type 'ConversionWebhook'"), @@ -624,7 +882,7 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { }, { name: "accepts bundles without conversion webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -634,17 +892,16 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { Type: v1alpha1.MutatingAdmissionWebhook, GenerateName: "test-mute-webhook", }, - ), - ), + ).Build(), }, }, { name: "accepts bundles with conversion webhooks that reference owned CRDs", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "some.crd.something"}, v1alpha1.CRDDescription{Name: "another.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -654,16 +911,15 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { "another.crd.something", }, }, - ), - ), + ).Build(), }, }, { name: "rejects bundles with conversion webhooks that reference existing CRDs that are not owned", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "some.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -673,8 +929,7 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { "another.crd.something", }, }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("conversion webhook 'test-webhook' references custom resource definition 'another.crd.something' not owned bundle"), @@ -682,10 +937,10 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { }, { name: "errors are ordered by webhook name and CRD name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "b.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -708,8 +963,7 @@ func Test_CheckConversionWebhooksReferenceOwnedCRDs(t *testing.T) { "d.crd.something", }, }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("conversion webhook 'test-webhook-a' references custom resource definition 'a.crd.something' not owned bundle"), @@ -740,7 +994,7 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { { name: "accepts bundles without conversion webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -750,19 +1004,18 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { Type: v1alpha1.MutatingAdmissionWebhook, GenerateName: "test-mute-webhook", }, - ), - ), + ).Build(), }, expectedErrs: []error{}, }, { name: "accepts bundles with conversion webhooks that reference different CRDs", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "some.crd.something"}, v1alpha1.CRDDescription{Name: "another.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -778,18 +1031,17 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { "another.crd.something", }, }, - ), - ), + ).Build(), }, expectedErrs: []error{}, }, { name: "rejects bundles with conversion webhooks that reference the same CRD", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "some.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -805,8 +1057,7 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { "some.crd.something", }, }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("conversion webhooks [test-webhook,test-webhook-two] reference same custom resource definition 'some.crd.something'"), @@ -815,10 +1066,10 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { { name: "errors are ordered by CRD name and webhook names", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithOwnedCRDs( v1alpha1.CRDDescription{Name: "b.crd.something"}, - ), + ). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ConversionWebhook, @@ -843,8 +1094,7 @@ func Test_CheckConversionWebhookCRDReferenceUniqueness(t *testing.T) { "d.crd.something", }, }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("conversion webhooks [test-webhook-a,test-webhook-b] reference same custom resource definition 'a.crd.something'"), @@ -869,12 +1119,12 @@ func Test_CheckWebhookNameIsDNS1123SubDomain(t *testing.T) { { name: "accepts bundles without webhook definitions", bundle: &bundle.RegistryV1{ - CSV: MakeCSV(), + CSV: clusterserviceversion.Builder().Build(), }, }, { name: "rejects bundles with invalid webhook definitions names and orders errors by webhook type and name", bundle: &bundle.RegistryV1{ - CSV: MakeCSV( + CSV: clusterserviceversion.Builder(). WithWebhookDefinitions( v1alpha1.WebhookDescription{ Type: v1alpha1.ValidatingAdmissionWebhook, @@ -902,8 +1152,7 @@ func Test_CheckWebhookNameIsDNS1123SubDomain(t *testing.T) { Type: v1alpha1.ConversionWebhook, GenerateName: "a-bad-name-", }, - ), - ), + ).Build(), }, expectedErrs: []error{ errors.New("webhook of type 'ConversionWebhook' has invalid name 'a-bad-name-': a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"), diff --git a/internal/operator-controller/rukpak/render/render.go b/internal/operator-controller/rukpak/render/render.go index 70063f1d48..f7e419c783 100644 --- a/internal/operator-controller/rukpak/render/render.go +++ b/internal/operator-controller/rukpak/render/render.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" @@ -12,6 +12,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" + hashutil "github.com/operator-framework/operator-controller/internal/shared/util/hash" ) // BundleValidator validates a RegistryV1 bundle by executing a series of @@ -54,7 +55,7 @@ func (r ResourceGenerators) ResourceGenerator() ResourceGenerator { return r.GenerateResources } -type UniqueNameGenerator func(string, interface{}) (string, error) +type UniqueNameGenerator func(string, interface{}) string type Options struct { InstallNamespace string @@ -74,9 +75,6 @@ func (o *Options) apply(opts ...Option) *Options { func (o *Options) validate(rv1 *bundle.RegistryV1) (*Options, []error) { var errs []error - if len(o.TargetNamespaces) == 0 { - errs = append(errs, errors.New("at least one target namespace must be specified")) - } if o.UniqueNameGenerator == nil { errs = append(errs, errors.New("unique name generator must be specified")) } @@ -88,9 +86,14 @@ func (o *Options) validate(rv1 *bundle.RegistryV1) (*Options, []error) { type Option func(*Options) +// WithTargetNamespaces sets the target namespaces to be used when rendering the bundle +// The value will only be used if len(namespaces) > 0. Otherwise, the default value for the bundle +// derived from its install mode support will be used (if such a value can be defined). func WithTargetNamespaces(namespaces ...string) Option { return func(o *Options) { - o.TargetNamespaces = namespaces + if len(namespaces) > 0 { + o.TargetNamespaces = namespaces + } } } @@ -121,7 +124,7 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o genOpts, errs := (&Options{ // default options InstallNamespace: installNamespace, - TargetNamespaces: []string{metav1.NamespaceAll}, + TargetNamespaces: defaultTargetNamespacesForBundle(&rv1), UniqueNameGenerator: DefaultUniqueNameGenerator, CertificateProvider: nil, }).apply(opts...).validate(&rv1) @@ -138,40 +141,71 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o return objs, nil } -func DefaultUniqueNameGenerator(base string, o interface{}) (string, error) { - hashStr, err := util.DeepHashObject(o) - if err != nil { - return "", err - } - return util.ObjectNameForBaseAndSuffix(base, hashStr), nil +func DefaultUniqueNameGenerator(base string, o interface{}) string { + hashStr := hashutil.DeepHashObject(o) + return util.ObjectNameForBaseAndSuffix(base, hashStr) } func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, targetNamespaces []string) error { - supportedInstallModes := sets.New[string]() - for _, im := range rv1.CSV.Spec.InstallModes { - if im.Supported { - supportedInstallModes.Insert(string(im.Type)) - } - } + supportedInstallModes := supportedBundleInstallModes(rv1) set := sets.New[string](targetNamespaces...) switch { - case set.Len() == 0 || (set.Len() == 1 && set.Has("")): - if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeAllNamespaces)) { + case set.Len() == 0: + // Note: this function generally expects targetNamespace to contain at least one value set by default + // in case the user does not specify the value. The option to set the targetNamespace is a no-op if it is empty. + // The only case for which a default targetNamespace is undefined is in the case of a bundle that only + // supports SingleNamespace install mode. The if statement here is added to provide a more friendly error + // message than just the generic (at least one target namespace must be specified) which would occur + // in case only the MultiNamespace install mode is supported by the bundle. + // If AllNamespaces mode is supported, the default will be [""] -> watch all namespaces + // If only OwnNamespace is supported, the default will be [install-namespace] -> only watch the install/own namespace + if supportedInstallModes.Has(v1alpha1.InstallModeTypeMultiNamespace) { + return errors.New("at least one target namespace must be specified") + } + return errors.New("exactly one target namespace must be specified") + case set.Len() == 1 && set.Has(""): + if supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces) { return nil } return fmt.Errorf("supported install modes %v do not support targeting all namespaces", sets.List(supportedInstallModes)) case set.Len() == 1 && !set.Has(""): - if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeSingleNamespace)) { + if targetNamespaces[0] == installNamespace { + if !supportedInstallModes.Has(v1alpha1.InstallModeTypeOwnNamespace) { + return fmt.Errorf("supported install modes %v do not support targeting own namespace", sets.List(supportedInstallModes)) + } return nil } - if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeOwnNamespace)) && targetNamespaces[0] == installNamespace { + if supportedInstallModes.Has(v1alpha1.InstallModeTypeSingleNamespace) { return nil } default: - if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeMultiNamespace)) && !set.Has("") { + if !supportedInstallModes.Has(v1alpha1.InstallModeTypeOwnNamespace) && set.Has(installNamespace) { + return fmt.Errorf("supported install modes %v do not support targeting own namespace", sets.List(supportedInstallModes)) + } + if supportedInstallModes.Has(v1alpha1.InstallModeTypeMultiNamespace) && !set.Has("") { return nil } } - return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[string](supportedInstallModes), targetNamespaces) + return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[v1alpha1.InstallModeType](supportedInstallModes), targetNamespaces) +} + +func defaultTargetNamespacesForBundle(rv1 *bundle.RegistryV1) []string { + supportedInstallModes := supportedBundleInstallModes(rv1) + + if supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces) { + return []string{corev1.NamespaceAll} + } + + return nil +} + +func supportedBundleInstallModes(rv1 *bundle.RegistryV1) sets.Set[v1alpha1.InstallModeType] { + supportedInstallModes := sets.New[v1alpha1.InstallModeType]() + for _, im := range rv1.CSV.Spec.InstallModes { + if im.Supported { + supportedInstallModes.Insert(im.Type) + } + } + return supportedInstallModes } diff --git a/internal/operator-controller/rukpak/render/render_test.go b/internal/operator-controller/rukpak/render/render_test.go index 0760c4fa10..ca14598896 100644 --- a/internal/operator-controller/rukpak/render/render_test.go +++ b/internal/operator-controller/rukpak/render/render_test.go @@ -16,13 +16,14 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) func Test_BundleRenderer_NoConfig(t *testing.T) { renderer := render.BundleRenderer{} objs, err := renderer.Render( bundle.RegistryV1{ - CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), }, "", nil) require.NoError(t, err) require.Empty(t, objs) @@ -61,6 +62,113 @@ func Test_BundleRenderer_CreatesCorrectDefaultOptions(t *testing.T) { _, _ = renderer.Render(bundle.RegistryV1{}, expectedInstallNamespace) } +func Test_BundleRenderer_DefaultTargetNamespaces(t *testing.T) { + for _, tc := range []struct { + name string + supportedInstallModes []v1alpha1.InstallModeType + expectedTargetNamespaces []string + expectedErrMsg string + }{ + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace, SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "No default when bundle install modes are {SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + } { + t.Run(tc.name, func(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + require.Equal(t, tc.expectedTargetNamespaces, opts.TargetNamespaces) + return nil, nil + }, + }, + } + _, err := renderer.Render(bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithName("test"). + WithInstallModeSupportFor(tc.supportedInstallModes...).Build(), + }, "some-namespace") + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + } + }) + } +} + func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { for _, tc := range []struct { name string @@ -70,17 +178,16 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { err error }{ { - name: "rejects empty targetNamespaces", + name: "accepts empty targetNamespaces (because it is ignored)", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), opts: []render.Option{ render.WithTargetNamespaces(), }, - err: errors.New("invalid option(s): at least one target namespace must be specified"), }, { name: "rejects nil unique name generator", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), opts: []render.Option{ render.WithUniqueNameGenerator(nil), }, @@ -88,7 +195,7 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { }, { name: "rejects all namespace install if AllNamespaces install mode is not supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces(corev1.NamespaceAll), }, @@ -96,15 +203,15 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { }, { name: "rejects own namespace install if only AllNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), opts: []render.Option{ render.WithTargetNamespaces("install-namespace"), }, - err: errors.New("invalid option(s): invalid target namespaces [install-namespace]: supported install modes [AllNamespaces] do not support target namespaces [install-namespace]"), + err: errors.New("invalid option(s): invalid target namespaces [install-namespace]: supported install modes [AllNamespaces] do not support targeting own namespace"), }, { name: "rejects install out of own namespace if only OwnNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces("not-install-namespace"), }, @@ -112,7 +219,7 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { }, { name: "rejects multi-namespace install if MultiNamespace install mode is not supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), opts: []render.Option{ render.WithTargetNamespaces("ns1", "ns2", "ns3"), }, @@ -120,7 +227,7 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { }, { name: "rejects if bundle supports no install modes", installNamespace: "install-namespace", - csv: MakeCSV(), + csv: clusterserviceversion.Builder().Build(), opts: []render.Option{ render.WithTargetNamespaces("some-namespace"), }, @@ -128,38 +235,46 @@ func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { }, { name: "accepts all namespace render if AllNamespaces install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), opts: []render.Option{ render.WithTargetNamespaces(""), }, }, { name: "accepts install namespace render if SingleNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces("some-namespace"), }, }, { name: "accepts all install namespace render if OwnNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces("install-namespace"), }, }, { name: "accepts single namespace render if SingleNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces("some-namespace"), }, }, { name: "accepts multi namespace render if MultiNamespace install mode is supported", installNamespace: "install-namespace", - csv: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeMultiNamespace)), + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeMultiNamespace).Build(), opts: []render.Option{ render.WithTargetNamespaces("n1", "n2", "n3"), }, + }, { + name: "reject multi namespace render if OwnNamespace install mode is not supported and target namespaces include install namespace", + installNamespace: "install-namespace", + csv: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeMultiNamespace).Build(), + opts: []render.Option{ + render.WithTargetNamespaces("n1", "n2", "n3", "install-namespace"), + }, + err: errors.New("invalid option(s): invalid target namespaces [n1 n2 n3 install-namespace]: supported install modes [MultiNamespace] do not support targeting own namespace"), }, } { t.Run(tc.name, func(t *testing.T) { @@ -199,10 +314,10 @@ func Test_WithUniqueNameGenerator(t *testing.T) { opts := &render.Options{ UniqueNameGenerator: render.DefaultUniqueNameGenerator, } - render.WithUniqueNameGenerator(func(s string, i interface{}) (string, error) { - return "a man needs a name", nil + render.WithUniqueNameGenerator(func(s string, i interface{}) string { + return "a man needs a name" })(opts) - generatedName, _ := opts.UniqueNameGenerator("", nil) + generatedName := opts.UniqueNameGenerator("", nil) require.Equal(t, "a man needs a name", generatedName) } @@ -226,7 +341,7 @@ func Test_BundleRenderer_CallsResourceGenerators(t *testing.T) { } objs, err := renderer.Render( bundle.RegistryV1{ - CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), }, "") require.NoError(t, err) require.Equal(t, []client.Object{&corev1.Namespace{}, &corev1.Service{}, &appsv1.Deployment{}}, objs) @@ -245,7 +360,7 @@ func Test_BundleRenderer_ReturnsResourceGeneratorErrors(t *testing.T) { } objs, err := renderer.Render( bundle.RegistryV1{ - CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), }, "") require.Nil(t, objs) require.Error(t, err) diff --git a/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go new file mode 100644 index 0000000000..b9d2d8c25a --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs.go @@ -0,0 +1,145 @@ +package bundlefs + +import ( + "fmt" + "path/filepath" + "strings" + "testing/fstest" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-registry/alpha/property" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + registry "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/operator-registry" +) + +const ( + BundlePathAnnotations = "metadata/annotations.yaml" + BundlePathProperties = "metadata/properties.yaml" + BundlePathManifests = "manifests" +) + +// BundleFSBuilder builds a registry+v1 bundle filesystem +type BundleFSBuilder interface { + WithPackageName(packageName string) BundleFSBuilder + WithChannels(channels ...string) BundleFSBuilder + WithDefaultChannel(channel string) BundleFSBuilder + WithBundleProperty(propertyType string, value string) BundleFSBuilder + WithBundleResource(resourceName string, resource client.Object) BundleFSBuilder + WithCSV(csv v1alpha1.ClusterServiceVersion) BundleFSBuilder + Build() fstest.MapFS +} + +// bundleFSBuilder builds a registry+v1 bundle filesystem +type bundleFSBuilder struct { + annotations *registry.Annotations + properties []property.Property + resources map[string]client.Object +} + +func Builder() BundleFSBuilder { + return &bundleFSBuilder{} +} + +// WithPackageName is an option for NewBundleFS used to set the package name annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithPackageName(packageName string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.PackageName = packageName + return b +} + +// WithChannels is an option for NewBundleFS used to set the channels annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithChannels(channels ...string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.Channels = strings.Join(channels, ",") + return b +} + +// WithDefaultChannel is an option for NewBundleFS used to set the channel annotation in the +// bundle filesystem metadata/annotations.yaml file +func (b *bundleFSBuilder) WithDefaultChannel(channel string) BundleFSBuilder { + if b.annotations == nil { + b.annotations = ®istry.Annotations{} + } + b.annotations.DefaultChannelName = channel + return b +} + +// WithBundleProperty is an options for NewBundleFS used to add a property to the list of properties +// in the bundle filesystem metadata/properties.yaml file +func (b *bundleFSBuilder) WithBundleProperty(propertyType string, value string) BundleFSBuilder { + b.properties = append(b.properties, property.Property{ + Type: propertyType, + Value: []byte(`"` + value + `"`), + }) + return b +} + +// WithBundleResource is an option for NewBundleFS use to add the yaml representation of resource to the +// path manifests/.yaml on the bundles filesystem +func (b *bundleFSBuilder) WithBundleResource(resourceName string, resource client.Object) BundleFSBuilder { + if b.resources == nil { + b.resources = make(map[string]client.Object) + } + b.resources[resourceName] = resource + return b +} + +// WithCSV is an optiona for NewBundleFS used to add the yaml representation of csv to the +// path manifests/csv.yaml on the bundle filesystem +func (b *bundleFSBuilder) WithCSV(csv v1alpha1.ClusterServiceVersion) BundleFSBuilder { + if b.resources == nil { + b.resources = make(map[string]client.Object) + } + b.resources["csv.yaml"] = &csv + return b +} + +// Build creates a registry+v1 bundle filesystem with the applied options +// By default, an empty registry+v1 bundle filesystem will be returned +func (b *bundleFSBuilder) Build() fstest.MapFS { + bundleFS := fstest.MapFS{} + + // Add annotations metadata + if b.annotations != nil { + annotationsYml, err := yaml.Marshal(registry.AnnotationsFile{ + Annotations: *b.annotations, + }) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[BundlePathAnnotations] = &fstest.MapFile{Data: annotationsYml} + } + + // Add property metadata + if len(b.properties) > 0 { + propertiesYml, err := yaml.Marshal(source.RegistryV1Properties{ + Properties: b.properties, + }) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[BundlePathProperties] = &fstest.MapFile{Data: propertiesYml} + } + + // Add resources + for name, obj := range b.resources { + resourcePath := filepath.Join(BundlePathManifests, name) + resourceYml, err := yaml.Marshal(obj) + if err != nil { + panic(fmt.Errorf("error building bundle fs: %w", err)) + } + bundleFS[resourcePath] = &fstest.MapFile{Data: resourceYml} + } + + return bundleFS +} diff --git a/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go new file mode 100644 index 0000000000..74e8410f35 --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/bundlefs/bundlefs_test.go @@ -0,0 +1,110 @@ +package bundlefs_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/bundlefs" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" +) + +func Test_BundleFSBuilder(t *testing.T) { + t.Run("returns empty bundle file system by default", func(t *testing.T) { + bundleFs := bundlefs.Builder().Build() + assert.Empty(t, bundleFs) + }) + + t.Run("WithPackageName sets the bundle package annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithPackageName("test").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: "" + operators.operatorframework.io.bundle.package.v1: test +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithChannels sets the bundle channels annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithChannels("alpha", "beta", "stable").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: alpha,beta,stable + operators.operatorframework.io.bundle.package.v1: "" +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithDefaultChannel sets the bundle default channel annotation", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithDefaultChannel("stable").Build() + require.Contains(t, bundleFs, "metadata/annotations.yaml") + require.Equal(t, []byte(`annotations: + operators.operatorframework.io.bundle.channel.default.v1: stable + operators.operatorframework.io.bundle.channels.v1: "" + operators.operatorframework.io.bundle.package.v1: "" +`), bundleFs["metadata/annotations.yaml"].Data) + }) + + t.Run("WithBundleProperty sets the bundle properties", func(t *testing.T) { + bundleFs := bundlefs.Builder(). + WithBundleProperty("foo", "bar"). + WithBundleProperty("key", "value"). + Build() + + require.Contains(t, bundleFs, "metadata/properties.yaml") + require.Equal(t, []byte(`properties: +- type: foo + value: bar +- type: key + value: value +`), bundleFs["metadata/properties.yaml"].Data) + }) + + t.Run("WithBundleResource adds a resource to the manifests directory", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }).Build() + require.Contains(t, bundleFs, "manifests/service.yaml") + require.Equal(t, []byte(`apiVersion: v1 +kind: Service +metadata: + name: test +spec: {} +status: + loadBalancer: {} +`), bundleFs["manifests/service.yaml"].Data) + }) + + t.Run("WithCSV adds a csv to the manifests directory", func(t *testing.T) { + bundleFs := bundlefs.Builder().WithCSV(clusterserviceversion.Builder().WithName("some-csv").Build()).Build() + require.Contains(t, bundleFs, "manifests/csv.yaml") + require.Equal(t, []byte(`apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: some-csv +spec: + apiservicedefinitions: {} + cleanup: + enabled: false + customresourcedefinitions: {} + displayName: "" + install: + spec: + deployments: null + strategy: "" + provider: {} + version: 0.0.0 +status: + cleanup: {} +`), bundleFs["manifests/csv.yaml"].Data) + }) +} diff --git a/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder.go b/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder.go new file mode 100644 index 0000000000..e7ae2195d9 --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder.go @@ -0,0 +1,103 @@ +package clusterserviceversion + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +var installModes = []v1alpha1.InstallModeType{ + v1alpha1.InstallModeTypeAllNamespaces, + v1alpha1.InstallModeTypeSingleNamespace, + v1alpha1.InstallModeTypeMultiNamespace, + v1alpha1.InstallModeTypeOwnNamespace, +} + +// ClusterServiceVersionBuilder build a ClusterServiceVersion resource +type ClusterServiceVersionBuilder interface { + WithName(name string) ClusterServiceVersionBuilder + WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) ClusterServiceVersionBuilder + WithAnnotations(annotations map[string]string) ClusterServiceVersionBuilder + WithPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) ClusterServiceVersionBuilder + WithClusterPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) ClusterServiceVersionBuilder + WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) ClusterServiceVersionBuilder + WithInstallModeSupportFor(installModeType ...v1alpha1.InstallModeType) ClusterServiceVersionBuilder + WithWebhookDefinitions(webhookDefinitions ...v1alpha1.WebhookDescription) ClusterServiceVersionBuilder + WithOwnedAPIServiceDescriptions(ownedAPIServiceDescriptions ...v1alpha1.APIServiceDescription) ClusterServiceVersionBuilder + Build() v1alpha1.ClusterServiceVersion +} + +// Builder creates a new ClusterServiceVersionBuilder for building ClusterServiceVersion resources +func Builder() ClusterServiceVersionBuilder { + return &clusterServiceVersionBuilder{ + csv: v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "ClusterServiceVersion", + }, + }, + } +} + +type clusterServiceVersionBuilder struct { + csv v1alpha1.ClusterServiceVersion +} + +//nolint:unparam +func (b *clusterServiceVersionBuilder) WithName(name string) ClusterServiceVersionBuilder { + b.csv.Name = name + return b +} + +func (b *clusterServiceVersionBuilder) WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) ClusterServiceVersionBuilder { + b.csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs + return b +} + +func (b *clusterServiceVersionBuilder) WithAnnotations(annotations map[string]string) ClusterServiceVersionBuilder { + b.csv.Annotations = annotations + return b +} + +func (b *clusterServiceVersionBuilder) WithPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) ClusterServiceVersionBuilder { + b.csv.Spec.InstallStrategy.StrategySpec.Permissions = permissions + return b +} + +func (b *clusterServiceVersionBuilder) WithClusterPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) ClusterServiceVersionBuilder { + b.csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions = permissions + return b +} + +func (b *clusterServiceVersionBuilder) WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) ClusterServiceVersionBuilder { + b.csv.Spec.CustomResourceDefinitions.Owned = crdDesc + return b +} + +func (b *clusterServiceVersionBuilder) WithInstallModeSupportFor(installModeType ...v1alpha1.InstallModeType) ClusterServiceVersionBuilder { + supportedInstallModes := sets.New(installModeType...) + csvInstallModes := make([]v1alpha1.InstallMode, 0, len(installModeType)) + for _, t := range installModes { + csvInstallModes = append(csvInstallModes, v1alpha1.InstallMode{ + Type: t, + Supported: supportedInstallModes.Has(t), + }) + } + b.csv.Spec.InstallModes = csvInstallModes + return b +} + +func (b *clusterServiceVersionBuilder) WithWebhookDefinitions(webhookDefinitions ...v1alpha1.WebhookDescription) ClusterServiceVersionBuilder { + b.csv.Spec.WebhookDefinitions = webhookDefinitions + return b +} + +func (b *clusterServiceVersionBuilder) WithOwnedAPIServiceDescriptions(ownedAPIServiceDescriptions ...v1alpha1.APIServiceDescription) ClusterServiceVersionBuilder { + b.csv.Spec.APIServiceDefinitions.Owned = ownedAPIServiceDescriptions + return b +} + +func (b *clusterServiceVersionBuilder) Build() v1alpha1.ClusterServiceVersion { + return b.csv +} diff --git a/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder_test.go b/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder_test.go new file mode 100644 index 0000000000..45bad4e53c --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/clusterserviceversion/builder_test.go @@ -0,0 +1,215 @@ +package clusterserviceversion_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" +) + +func Test_Builder(t *testing.T) { + t.Run("builds an empty csv by default", func(t *testing.T) { + obj := clusterserviceversion.Builder().Build() + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + }, obj) + }) + + t.Run("WithName sets csv .metadata.name", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithName("some-name").Build() + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, obj) + }) + + t.Run("WithStrategyDeploymentSpecs sets csv .spec.install.spec.deployments", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "spec-one", + }, + v1alpha1.StrategyDeploymentSpec{ + Name: "spec-two", + }, + ).Build() + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ + { + Name: "spec-one", + }, + { + Name: "spec-two", + }, + }, + }, + }, + }, + }, obj) + }) + + t.Run("WithPermissions sets csv .spec.install.spec.permissions", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + }, + ).Build() + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + Permissions: []v1alpha1.StrategyDeploymentPermissions{ + { + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + { + ServiceAccountName: "", + }, + }, + }, + }, + }, + }, obj) + }) + + t.Run("WithClusterPermissions sets csv .spec.install.spec.clusterPermissions", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + }, + ).Build() + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + ClusterPermissions: []v1alpha1.StrategyDeploymentPermissions{ + { + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + { + ServiceAccountName: "", + }, + }, + }, + }, + }, + }, obj) + }) + + t.Run("WithClusterPermissions sets csv .spec.customresourcedefinitions.owned", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithOwnedCRDs( + v1alpha1.CRDDescription{Name: "a.crd.something"}, + v1alpha1.CRDDescription{Name: "b.crd.something"}, + ).Build() + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + {Name: "a.crd.something"}, + {Name: "b.crd.something"}, + }, + }, + }, + }, obj) + }) + + t.Run("WithInstallModeSupportFor adds all install modes to .spec.installModes and sets supported to true for the given supported install modes", func(t *testing.T) { + obj := clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace).Build() + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallModes: []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeMultiNamespace, + Supported: false, + }, + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: false, + }, + }, + }, + }, obj) + }) +} diff --git a/internal/operator-controller/rukpak/util/testing/testing.go b/internal/operator-controller/rukpak/util/testing/testing.go index f5c9b36a38..2670091a19 100644 --- a/internal/operator-controller/rukpak/util/testing/testing.go +++ b/internal/operator-controller/rukpak/util/testing/testing.go @@ -4,94 +4,14 @@ import ( "testing" "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/operator-framework/api/pkg/operators/v1alpha1" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" ) -type CSVOption func(version *v1alpha1.ClusterServiceVersion) - -//nolint:unparam -func WithName(name string) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Name = name - } -} - -func WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs - } -} - -func WithAnnotations(annotations map[string]string) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Annotations = annotations - } -} - -func WithPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.InstallStrategy.StrategySpec.Permissions = permissions - } -} - -func WithClusterPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions = permissions - } -} - -func WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.CustomResourceDefinitions.Owned = crdDesc - } -} - -func WithInstallModeSupportFor(installModeType ...v1alpha1.InstallModeType) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - installModes := make([]v1alpha1.InstallMode, 0, len(installModeType)) - for _, t := range installModeType { - installModes = append(installModes, v1alpha1.InstallMode{ - Type: t, - Supported: true, - }) - } - csv.Spec.InstallModes = installModes - } -} - -func WithWebhookDefinitions(webhookDefinitions ...v1alpha1.WebhookDescription) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.WebhookDefinitions = webhookDefinitions - } -} - -func WithOwnedAPIServiceDescriptions(ownedAPIServiceDescriptions ...v1alpha1.APIServiceDescription) CSVOption { - return func(csv *v1alpha1.ClusterServiceVersion) { - csv.Spec.APIServiceDefinitions.Owned = ownedAPIServiceDescriptions - } -} - -func MakeCSV(opts ...CSVOption) v1alpha1.ClusterServiceVersion { - csv := v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: "ClusterServiceVersion", - }, - } - for _, opt := range opts { - opt(&csv) - } - return csv -} - type FakeCertProvider struct { InjectCABundleFn func(obj client.Object, cfg render.CertificateProvisionerConfig) error AdditionalObjectsFn func(cfg render.CertificateProvisionerConfig) ([]unstructured.Unstructured, error) diff --git a/internal/operator-controller/rukpak/util/testing/testing_test.go b/internal/operator-controller/rukpak/util/testing/testing_test.go deleted file mode 100644 index c8744cfa0a..0000000000 --- a/internal/operator-controller/rukpak/util/testing/testing_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package testing_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/operator-framework/api/pkg/operators/v1alpha1" - - . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" -) - -func Test_MakeCSV(t *testing.T) { - csv := MakeCSV() - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - }, csv) -} - -func Test_MakeCSV_WithName(t *testing.T) { - csv := MakeCSV(WithName("some-name")) - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "some-name", - }, - }, csv) -} - -func Test_MakeCSV_WithStrategyDeploymentSpecs(t *testing.T) { - csv := MakeCSV( - WithStrategyDeploymentSpecs( - v1alpha1.StrategyDeploymentSpec{ - Name: "spec-one", - }, - v1alpha1.StrategyDeploymentSpec{ - Name: "spec-two", - }, - ), - ) - - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategySpec: v1alpha1.StrategyDetailsDeployment{ - DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ - { - Name: "spec-one", - }, - { - Name: "spec-two", - }, - }, - }, - }, - }, - }, csv) -} - -func Test_MakeCSV_WithPermissions(t *testing.T) { - csv := MakeCSV( - WithPermissions( - v1alpha1.StrategyDeploymentPermissions{ - ServiceAccountName: "service-account", - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"list", "watch"}, - }, - }, - }, - v1alpha1.StrategyDeploymentPermissions{ - ServiceAccountName: "", - }, - ), - ) - - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategySpec: v1alpha1.StrategyDetailsDeployment{ - Permissions: []v1alpha1.StrategyDeploymentPermissions{ - { - ServiceAccountName: "service-account", - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"list", "watch"}, - }, - }, - }, - { - ServiceAccountName: "", - }, - }, - }, - }, - }, - }, csv) -} - -func Test_MakeCSV_WithClusterPermissions(t *testing.T) { - csv := MakeCSV( - WithClusterPermissions( - v1alpha1.StrategyDeploymentPermissions{ - ServiceAccountName: "service-account", - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"list", "watch"}, - }, - }, - }, - v1alpha1.StrategyDeploymentPermissions{ - ServiceAccountName: "", - }, - ), - ) - - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategySpec: v1alpha1.StrategyDetailsDeployment{ - ClusterPermissions: []v1alpha1.StrategyDeploymentPermissions{ - { - ServiceAccountName: "service-account", - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"list", "watch"}, - }, - }, - }, - { - ServiceAccountName: "", - }, - }, - }, - }, - }, - }, csv) -} - -func Test_MakeCSV_WithOwnedCRDs(t *testing.T) { - csv := MakeCSV( - WithOwnedCRDs( - v1alpha1.CRDDescription{Name: "a.crd.something"}, - v1alpha1.CRDDescription{Name: "b.crd.something"}, - ), - ) - - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: []v1alpha1.CRDDescription{ - {Name: "a.crd.something"}, - {Name: "b.crd.something"}, - }, - }, - }, - }, csv) -} - -func Test_MakeCSV_WithInstallModeSupportFor(t *testing.T) { - csv := MakeCSV( - WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), - ) - - require.Equal(t, v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterServiceVersion", - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallModes: []v1alpha1.InstallMode{ - { - Type: v1alpha1.InstallModeTypeAllNamespaces, - Supported: true, - }, - { - Type: v1alpha1.InstallModeTypeSingleNamespace, - Supported: true, - }, - }, - }, - }, csv) -} diff --git a/internal/operator-controller/rukpak/util/util.go b/internal/operator-controller/rukpak/util/util.go index 503d7afa78..067722c47e 100644 --- a/internal/operator-controller/rukpak/util/util.go +++ b/internal/operator-controller/rukpak/util/util.go @@ -41,7 +41,6 @@ func ToUnstructured(obj client.Object) (*unstructured.Unstructured, error) { return nil, fmt.Errorf("convert %s %q to unstructured: %w", gvk.Kind, obj.GetName(), err) } unstructured.RemoveNestedField(uObj, "metadata", "creationTimestamp") - unstructured.RemoveNestedField(uObj, "status") u.Object = uObj u.SetGroupVersionKind(gvk) return &u, nil diff --git a/internal/operator-controller/rukpak/util/util_test.go b/internal/operator-controller/rukpak/util/util_test.go index 60c1cd6462..25073f0ce4 100644 --- a/internal/operator-controller/rukpak/util/util_test.go +++ b/internal/operator-controller/rukpak/util/util_test.go @@ -127,7 +127,7 @@ spec: if tc.wantErr { require.Error(t, err) } else { - assert.Equal(t, len(objs), len(tc.expectObjects)) + assert.Len(t, tc.expectObjects, len(objs)) // Sort the objs by name for easy direct comparison sort.Slice(objs, func(i int, j int) bool { return objs[i].GetName() < objs[j].GetName() diff --git a/internal/shared/controllers/pull_secret_controller.go b/internal/shared/controllers/pull_secret_controller.go new file mode 100644 index 0000000000..43a2ceec80 --- /dev/null +++ b/internal/shared/controllers/pull_secret_controller.go @@ -0,0 +1,243 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/go-logr/logr" + "github.com/google/renameio/v2" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// PullSecretReconciler reconciles a specific Secret object +// that contains global pull secrets for pulling Catalog images +type PullSecretReconciler struct { + client.Client + SecretKey *types.NamespacedName + ServiceAccountKey types.NamespacedName + ServiceAccountPullSecrets []types.NamespacedName + AuthFilePath string +} + +func (r *PullSecretReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithName("pull-secret-reconciler") + + logger.Info("starting reconciliation") + defer logger.Info("finishing reconciliation") + + secrets := []*corev1.Secret{} + + if r.SecretKey != nil { + secret, err := r.getSecret(ctx, logger, *r.SecretKey) + if err != nil { + return ctrl.Result{}, err + } + // Add the configured pull secret to the list of secrets + if secret != nil { + secrets = append(secrets, secret) + } + } + + // Grab all the pull secrets from the serviceaccount and add them to the list of secrets + sa := &corev1.ServiceAccount{} + if err := r.Get(ctx, r.ServiceAccountKey, sa); err != nil { //nolint:nestif + if apierrors.IsNotFound(err) { + logger.Info("serviceaccount not found", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + } else { + logger.Error(err, "failed to get serviceaccount", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + return ctrl.Result{}, err + } + } else { + logger.Info("found serviceaccount", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + nn := types.NamespacedName{Namespace: r.ServiceAccountKey.Namespace} + pullSecrets := []types.NamespacedName{} + for _, ips := range sa.ImagePullSecrets { + nn.Name = ips.Name + // This is to update the list of secrets that we are filtering on + // Add all secrets regardless if they exist or not + pullSecrets = append(pullSecrets, nn) + + secret, err := r.getSecret(ctx, logger, nn) + if err != nil { + return ctrl.Result{}, err + } + if secret != nil { + secrets = append(secrets, secret) + } + } + // update list of pull secrets from service account + r.ServiceAccountPullSecrets = pullSecrets + // Log ever-so slightly nicer + names := []string{} + for _, ps := range pullSecrets { + names = append(names, logNamespacedName(ps)) + } + logger.Info("updating list of pull-secrets", "pull-secrets", names) + } + + if len(secrets) == 0 { + return ctrl.Result{}, r.deleteSecretFile(logger) + } + return ctrl.Result{}, r.writeSecretToFile(logger, secrets) +} + +func (r *PullSecretReconciler) getSecret(ctx context.Context, logger logr.Logger, nn types.NamespacedName) (*corev1.Secret, error) { + secret := &corev1.Secret{} + if err := r.Get(ctx, nn, secret); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("pull-secret not found", "pull-secret", logNamespacedName(nn)) + return nil, nil + } + logger.Error(err, "failed to get pull-secret", "pull-secret", logNamespacedName(nn)) + return nil, err + } + logger.Info("found pull-secret", "pull-secret", logNamespacedName(nn)) + return secret, nil +} + +func logNamespacedName(nn types.NamespacedName) string { + return fmt.Sprintf("%s/%s", nn.Namespace, nn.Name) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + _, err := ctrl.NewControllerManagedBy(mgr). + For(&corev1.Secret{}). + Named("pull-secret-controller"). + WithEventFilter(newSecretPredicate(r)). + Build(r) + if err != nil { + return err + } + + _, err = ctrl.NewControllerManagedBy(mgr). + For(&corev1.ServiceAccount{}). + Named("service-account-controller"). + WithEventFilter(newNamespacedPredicate(r.ServiceAccountKey)). + Build(r) + + return err +} + +// Filters based on the global SecretKey, or any pull secret from the serviceaccount +func newSecretPredicate(r *PullSecretReconciler) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + nn := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()} + if r.SecretKey != nil && nn == *r.SecretKey { + return true + } + for _, ps := range r.ServiceAccountPullSecrets { + if nn == ps { + return true + } + } + return false + }) +} + +func newNamespacedPredicate(key types.NamespacedName) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace + }) +} + +// Golang representation of the docker configuration - either dockerconfigjson or dockercfg formats. +// This allows us to merge the two formats together, regardless of type, and dump it out as a +// dockerconfigjson for use my contaners/images +type dockerConfigJSON struct { + Auths dockerCfg `json:"auths"` +} + +type dockerCfg map[string]authEntries + +type authEntries struct { + Auth string `json:"auth"` + Email string `json:"email,omitempty"` +} + +// writeSecretToFile writes the secret data to the specified file +func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secrets []*corev1.Secret) error { + // image registry secrets are always stored with the key .dockerconfigjson or .dockercfg + // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials + // expected format for auth.json + // ref: https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md + + jsonData := dockerConfigJSON{} + jsonData.Auths = make(dockerCfg) + + for _, s := range secrets { + if secretData, ok := s.Data[".dockerconfigjson"]; ok { + // process as dockerconfigjson + dcj := &dockerConfigJSON{} + if err := json.Unmarshal(secretData, dcj); err != nil { + return err + } + for n, v := range dcj.Auths { + jsonData.Auths[n] = v + } + continue + } + if secretData, ok := s.Data[".dockercfg"]; ok { + // process as dockercfg, despite being a map, this has to be Unmarshal'd as a pointer + dc := &dockerCfg{} + if err := json.Unmarshal(secretData, dc); err != nil { + return err + } + for n, v := range *dc { + jsonData.Auths[n] = v + } + continue + } + // Ignore the unknown secret + logger.Info("expected secret.Data key not found", "pull-secret", logNamespacedName(types.NamespacedName{Name: s.Name, Namespace: s.Namespace})) + } + + data, err := json.Marshal(jsonData) + if err != nil { + return fmt.Errorf("failed to marshal secret data: %w", err) + } + err = renameio.WriteFile(r.AuthFilePath, data, 0600) + if err != nil { + return fmt.Errorf("failed to write secret data to file: %w", err) + } + logger.Info("saved global pull secret data locally") + return nil +} + +// deleteSecretFile deletes the auth file if the secret is deleted +func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) error { + logger.Info("deleting local auth file", "file", r.AuthFilePath) + if err := os.Remove(r.AuthFilePath); err != nil { + if os.IsNotExist(err) { + logger.Info("auth file does not exist, nothing to delete") + return nil + } + return fmt.Errorf("failed to delete secret file: %w", err) + } + logger.Info("auth file deleted successfully") + return nil +} diff --git a/internal/shared/controllers/pull_secret_controller_test.go b/internal/shared/controllers/pull_secret_controller_test.go new file mode 100644 index 0000000000..1619267551 --- /dev/null +++ b/internal/shared/controllers/pull_secret_controller_test.go @@ -0,0 +1,154 @@ +package controllers + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestSecretSyncerReconciler(t *testing.T) { + secretFullData := []byte(`{"auths":{"exampleRegistry": {"auth": "exampledata"}}}`) + secretPartData := []byte(`{"exampleRegistry": {"auth": "exampledata"}}`) + authFileName := "test-auth.json" + for _, tt := range []struct { + name string + secretKey *types.NamespacedName + sa *corev1.ServiceAccount + secrets []corev1.Secret + wantErr string + fileShouldExistBefore bool + fileShouldExistAfter bool + }{ + { + name: "secret exists, dockerconfigjson content gets saved to authFile", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + { + name: "secret exists, dockercfg content gets saved to authFile", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockercfg": secretPartData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + { + name: "secret does not exist, file exists previously, file should get deleted", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + fileShouldExistBefore: true, + fileShouldExistAfter: false, + }, + { + name: "serviceaccount secrets, both dockerconfigjson and dockercfg content gets saved to authFile", + sa: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + Namespace: "test-secret-namespace", + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "test-secret1"}, + {Name: "test-secret2"}, + }, + }, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret1", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret2", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + tempAuthFile := filepath.Join(t.TempDir(), authFileName) + clientBuilder := fake.NewClientBuilder() + for _, ps := range tt.secrets { + clientBuilder = clientBuilder.WithObjects(ps.DeepCopy()) + } + if tt.sa != nil { + clientBuilder = clientBuilder.WithObjects(tt.sa) + } + cl := clientBuilder.Build() + + var triggerKey types.NamespacedName + if tt.secretKey != nil { + triggerKey = *tt.secretKey + } + var saKey types.NamespacedName + if tt.sa != nil { + saKey = types.NamespacedName{Namespace: tt.sa.Namespace, Name: tt.sa.Name} + triggerKey = saKey + } + r := &PullSecretReconciler{ + Client: cl, + SecretKey: tt.secretKey, + ServiceAccountKey: saKey, + AuthFilePath: tempAuthFile, + } + if tt.fileShouldExistBefore { + err := os.WriteFile(tempAuthFile, secretFullData, 0600) + require.NoError(t, err) + } + res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: triggerKey}) + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + require.Equal(t, ctrl.Result{}, res) + + if tt.fileShouldExistAfter { + _, err := os.Stat(tempAuthFile) + require.NoError(t, err) + } else { + _, err := os.Stat(tempAuthFile) + require.True(t, os.IsNotExist(err)) + } + }) + } +} diff --git a/internal/operator-controller/rukpak/util/hash.go b/internal/shared/util/hash/hash.go similarity index 62% rename from internal/operator-controller/rukpak/util/hash.go rename to internal/shared/util/hash/hash.go index a46bb34347..c1fac21bf5 100644 --- a/internal/operator-controller/rukpak/util/hash.go +++ b/internal/shared/util/hash/hash.go @@ -1,4 +1,4 @@ -package util +package hash import ( "crypto/sha256" @@ -7,10 +7,10 @@ import ( "math/big" ) -// DeepHashObject writes specified object to hash using the spew library -// which follows pointers and prints actual values of the nested objects -// ensuring the hash does not change when a pointer changes. -func DeepHashObject(obj interface{}) (string, error) { +// DeepHashObject writes specified object to hash based on the object's +// JSON representation. If the object fails JSON marshaling, DeepHashObject +// panics. +func DeepHashObject(obj interface{}) string { // While the most accurate encoding we could do for Kubernetes objects (runtime.Object) // would use the API machinery serializers, those operate over entire objects - and // we often need to operate on snippets. Checking with the experts and the implementation, @@ -22,18 +22,19 @@ func DeepHashObject(obj interface{}) (string, error) { // will be encoded hasher := sha256.New224() - hasher.Reset() encoder := json.NewEncoder(hasher) if err := encoder.Encode(obj); err != nil { - return "", fmt.Errorf("couldn't encode object: %w", err) + panic(fmt.Sprintf("couldn't encode object: %v", err)) } - // base62(sha224(bytes)) is a useful hash and encoding for adding the contents of this + // TODO: Investigate whether we can change this to base62(sha224(bytes)) + // The main concern with changing the base is that it will cause the hash + // function output to change, which may cause issues with consumers expecting + // a consistent hash. + // + // base36(sha224(bytes)) is a useful hash and encoding for adding the contents of this // to a Kubernetes identifier or other field which has length and character set requirements - var hash []byte - hash = hasher.Sum(hash) - var i big.Int - i.SetBytes(hash[:]) - return i.Text(36), nil + i.SetBytes(hasher.Sum(nil)) + return i.Text(36) } diff --git a/internal/operator-controller/rukpak/util/hash_test.go b/internal/shared/util/hash/hash_test.go similarity index 75% rename from internal/operator-controller/rukpak/util/hash_test.go rename to internal/shared/util/hash/hash_test.go index 9d16dfd646..9e94ffc3eb 100644 --- a/internal/operator-controller/rukpak/util/hash_test.go +++ b/internal/shared/util/hash/hash_test.go @@ -1,24 +1,30 @@ -package util_test +package hash_test import ( + "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" + hashutil "github.com/operator-framework/operator-controller/internal/shared/util/hash" ) +type unmarshalable struct{} + +func (u *unmarshalable) MarshalJSON() ([]byte, error) { + return nil, errors.New("unmarshalable") +} + func TestDeepHashObject(t *testing.T) { tests := []struct { name string - wantErr bool + wantPanic bool obj interface{} expectedHash string }{ { - name: "populated obj with exported fields", - wantErr: false, + name: "populated obj with exported fields", obj: struct { Str string Num int @@ -37,8 +43,7 @@ func TestDeepHashObject(t *testing.T) { expectedHash: "gta1bt5sybll5qjqxdiekmjm7py93glrinmnrfb31fj", }, { - name: "modified populated obj with exported fields", - wantErr: false, + name: "modified populated obj with exported fields", obj: struct { Str string Num int @@ -57,8 +62,7 @@ func TestDeepHashObject(t *testing.T) { expectedHash: "1ftn1z2ieih8hsmi2a8c6mkoef6uodrtn4wtt1qapioh", }, { - name: "populated obj with unexported fields", - wantErr: false, + name: "populated obj with unexported fields", obj: struct { str string num int @@ -80,38 +84,42 @@ func TestDeepHashObject(t *testing.T) { // The JSON encoder requires exported fields or it will generate // the same hash as a completely empty object name: "empty obj", - wantErr: false, obj: struct{}{}, expectedHash: "16jfjhihxbzhfhs1k5mimq740kvioi98pfbea9q6qtf9", }, { name: "string a", - wantErr: false, obj: "a", expectedHash: "1lu1qv1451mq7gv9upu1cx8ffffi07rel5xvbvvc44dh", }, { name: "string b", - wantErr: false, obj: "b", expectedHash: "1ija85ah4gd0beltpfhszipkxfyqqxhp94tf2mjfgq61", }, { name: "nil obj", - wantErr: false, obj: nil, expectedHash: "2im0kl1kwvzn46sr4cdtkvmdzrlurvj51xdzhwdht8l0", }, + { + name: "unmarshalable obj", + obj: &unmarshalable{}, + wantPanic: true, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - hash, err := util.DeepHashObject(tc.obj) - if tc.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) + test := func() { + hash := hashutil.DeepHashObject(tc.obj) assert.Equal(t, tc.expectedHash, hash) } + + if tc.wantPanic { + require.Panics(t, test) + } else { + require.NotPanics(t, test) + } }) } } diff --git a/internal/shared/util/http/certlog.go b/internal/shared/util/http/certlog.go index 60aa0bd198..b772f93f1e 100644 --- a/internal/shared/util/http/certlog.go +++ b/internal/shared/util/http/certlog.go @@ -122,7 +122,16 @@ func logFile(filename, path, action string, log logr.Logger) { log.Error(err, "error in os.ReadFile()", "file", filepath) return } - logPem(data, filename, path, action, log) + if len(data) > 0 { + logPem(data, filename, path, action, log) + return + } + // Indicate that the file is empty + args := []any{"file", filename, "empty", "true"} + if path != "" { + args = append(args, "directory", path) + } + log.V(defaultLogLevel).Info(action, args...) } func logPem(data []byte, filename, path, action string, log logr.Logger) { diff --git a/internal/shared/util/http/certpoolwatcher.go b/internal/shared/util/http/certpoolwatcher.go index 7f95449e98..da5e78ba93 100644 --- a/internal/shared/util/http/certpoolwatcher.go +++ b/internal/shared/util/http/certpoolwatcher.go @@ -1,6 +1,7 @@ package http import ( + "context" "crypto/x509" "fmt" "os" @@ -14,13 +15,15 @@ import ( ) type CertPoolWatcher struct { - generation int - dir string - mx sync.RWMutex - pool *x509.CertPool - log logr.Logger - watcher *fsnotify.Watcher - done chan bool + generation int + dir string + sslCertPaths []string + mx sync.RWMutex + pool *x509.CertPool + log logr.Logger + watcher *fsnotify.Watcher + done chan bool + restart func(int) } // Returns the current CertPool and the generation number @@ -33,77 +36,111 @@ func (cpw *CertPoolWatcher) Get() (*x509.CertPool, int, error) { return cpw.pool.Clone(), cpw.generation, nil } -func (cpw *CertPoolWatcher) Done() { - cpw.done <- true +// Change the restart behavior +func (cpw *CertPoolWatcher) Restart(f func(int)) { + cpw.restart = f } -func NewCertPoolWatcher(caDir string, log logr.Logger) (*CertPoolWatcher, error) { - pool, err := NewCertPool(caDir, log) - if err != nil { - return nil, err +// Indicate that you're done with the CertPoolWatcher so it can terminate +// the watcher go func +func (cpw *CertPoolWatcher) Done() { + if cpw.watcher != nil { + cpw.done <- true } - watcher, err := fsnotify.NewWatcher() +} + +func (cpw *CertPoolWatcher) Start(ctx context.Context) error { + var err error + cpw.pool, err = NewCertPool(cpw.dir, cpw.log) if err != nil { - return nil, err + return err } - // If the SSL_CERT_DIR or SSL_CERT_FILE environment variables are - // specified, this means that we have some control over the system root - // location, thus they may change, thus we should watch those locations. - sslCertDir := os.Getenv("SSL_CERT_DIR") - sslCertFile := os.Getenv("SSL_CERT_FILE") - log.V(defaultLogLevel).Info("SSL environment", "SSL_CERT_DIR", sslCertDir, "SSL_CERT_FILE", sslCertFile) + watchPaths := append(cpw.sslCertPaths, cpw.dir) + watchPaths = slices.DeleteFunc(watchPaths, deleteEmptyStrings) - watchPaths := strings.Split(sslCertDir, ":") - watchPaths = append(watchPaths, caDir, sslCertFile) - watchPaths = slices.DeleteFunc(watchPaths, func(p string) bool { - if p == "" { - return true - } - if _, err := os.Stat(p); err != nil { - return true - } - return false - }) + // Nothing was configured to be watched, which means this is + // using the SystemCertPool, so we still need to no error out + if len(watchPaths) == 0 { + cpw.log.Info("No paths to watch") + return nil + } + + cpw.watcher, err = fsnotify.NewWatcher() + if err != nil { + return err + } for _, p := range watchPaths { - if err := watcher.Add(p); err != nil { - return nil, err + if err := cpw.watcher.Add(p); err != nil { + cpw.watcher.Close() + cpw.watcher = nil + return err } - logPath(p, "watching certificate", log) + logPath(p, "watching certificate", cpw.log) } - cpw := &CertPoolWatcher{ - generation: 1, - dir: caDir, - pool: pool, - log: log, - watcher: watcher, - done: make(chan bool), - } go func() { for { select { - case <-watcher.Events: + case e := <-cpw.watcher.Events: + cpw.checkForRestart(e.Name) cpw.drainEvents() - cpw.update() - case err := <-watcher.Errors: - log.Error(err, "error watching certificate dir") + cpw.update(e.Name) + case err := <-cpw.watcher.Errors: + cpw.log.Error(err, "error watching certificate dir") os.Exit(1) + case <-ctx.Done(): + cpw.Done() case <-cpw.done: - err := watcher.Close() + err := cpw.watcher.Close() if err != nil { - log.Error(err, "error closing watcher") + cpw.log.Error(err, "error closing watcher") } return } } }() + return nil +} + +func NewCertPoolWatcher(caDir string, log logr.Logger) (*CertPoolWatcher, error) { + // If the SSL_CERT_DIR or SSL_CERT_FILE environment variables are + // specified, this means that we have some control over the system root + // location, thus they may change, thus we should watch those locations. + // + // BECAUSE THE SYSTEM POOL WILL NOT UPDATE, WE HAVE TO RESTART IF THERE + // CHANGES TO EITHER OF THESE LOCATIONS: SSL_CERT_DIR, SSL_CERT_FILE + // + sslCertDir := os.Getenv("SSL_CERT_DIR") + sslCertFile := os.Getenv("SSL_CERT_FILE") + log.V(defaultLogLevel).Info("SSL environment", "SSL_CERT_DIR", sslCertDir, "SSL_CERT_FILE", sslCertFile) + + sslCertPaths := append(strings.Split(sslCertDir, ":"), sslCertFile) + sslCertPaths = slices.DeleteFunc(sslCertPaths, deleteEmptyStrings) + + cpw := &CertPoolWatcher{ + generation: 1, + dir: caDir, + sslCertPaths: sslCertPaths, + log: log, + done: make(chan bool), + } return cpw, nil } -func (cpw *CertPoolWatcher) update() { - cpw.log.Info("updating certificate pool") +func deleteEmptyStrings(p string) bool { + if p == "" { + return true + } + if _, err := os.Stat(p); err != nil { + return true + } + return false +} + +func (cpw *CertPoolWatcher) update(name string) { + cpw.log.Info("updating certificate pool", "file", name) pool, err := NewCertPool(cpw.dir, cpw.log) if err != nil { cpw.log.Error(err, "error updating certificate pool") @@ -115,6 +152,17 @@ func (cpw *CertPoolWatcher) update() { cpw.generation++ } +func (cpw *CertPoolWatcher) checkForRestart(name string) { + for _, p := range cpw.sslCertPaths { + if strings.Contains(name, p) { + cpw.log.Info("restarting due to file change", "file", name) + if cpw.restart != nil { + cpw.restart(0) + } + } + } +} + // Drain as many events as possible before doing anything // Otherwise, we will be hit with an event for _every_ entry in the // directory, and end up doing an update for each one @@ -124,7 +172,8 @@ func (cpw *CertPoolWatcher) drainEvents() { select { case <-drainTimer.C: return - case <-cpw.watcher.Events: + case e := <-cpw.watcher.Events: + cpw.checkForRestart(e.Name) } if !drainTimer.Stop() { <-drainTimer.C diff --git a/internal/shared/util/http/certpoolwatcher_test.go b/internal/shared/util/http/certpoolwatcher_test.go index ca13a478b6..f7d0a9f82d 100644 --- a/internal/shared/util/http/certpoolwatcher_test.go +++ b/internal/shared/util/http/certpoolwatcher_test.go @@ -11,6 +11,7 @@ import ( "math/big" "os" "path/filepath" + "sync/atomic" "testing" "time" @@ -61,7 +62,100 @@ func createCert(t *testing.T, name string) { // ignore the key } -func TestCertPoolWatcher(t *testing.T) { +func createTempCaDir(t *testing.T) string { + tmpCaDir, err := os.MkdirTemp("", "ca-dir") + require.NoError(t, err) + createCert(t, filepath.Join(tmpCaDir, "test1.pem")) + return tmpCaDir +} + +func TestCertPoolWatcherCaDir(t *testing.T) { + // create a temporary CA directory + tmpCaDir := createTempCaDir(t) + defer os.RemoveAll(tmpCaDir) + + os.Unsetenv("SSL_CERT_FILE") + os.Unsetenv("SSL_CERT_DIR") + + // Create the cert pool watcher + cpw, err := httputil.NewCertPoolWatcher(tmpCaDir, log.FromContext(context.Background())) + require.NoError(t, err) + require.NotNil(t, cpw) + defer cpw.Done() + restarted := &atomic.Bool{} + restarted.Store(false) + cpw.Restart(func(int) { restarted.Store(true) }) + err = cpw.Start(context.Background()) + require.NoError(t, err) + + // Get the original pool + firstPool, firstGen, err := cpw.Get() + require.NoError(t, err) + require.NotNil(t, firstPool) + + // Create a second cert in the CA directory + certName := filepath.Join(tmpCaDir, "test2.pem") + t.Logf("Create cert file at %q\n", certName) + createCert(t, certName) + + require.Eventually(t, func() bool { + secondPool, secondGen, err := cpw.Get() + if err != nil { + return false + } + // Should NOT restart, because this is not SSL_CERT_DIR nor SSL_CERT_FILE + return secondGen != firstGen && !firstPool.Equal(secondPool) && !restarted.Load() + }, 10*time.Second, time.Second) +} + +func TestCertPoolWatcherSslCertDir(t *testing.T) { + // create a temporary CA directory for SSL_CERT_DIR + tmpSslDir := createTempCaDir(t) + defer os.RemoveAll(tmpSslDir) + + // Update environment variables for the watcher - some of these should not exist + os.Unsetenv("SSL_CERT_FILE") + os.Setenv("SSL_CERT_DIR", tmpSslDir+":/tmp/does-not-exist.dir") + defer os.Unsetenv("SSL_CERT_DIR") + + // Create a different CaDir + tmpCaDir := createTempCaDir(t) + defer os.RemoveAll(tmpCaDir) + + // Create the cert pool watcher + cpw, err := httputil.NewCertPoolWatcher(tmpCaDir, log.FromContext(context.Background())) + require.NoError(t, err) + restarted := &atomic.Bool{} + restarted.Store(false) + cpw.Restart(func(int) { restarted.Store(true) }) + err = cpw.Start(context.Background()) + require.NoError(t, err) + defer cpw.Done() + + // Get the original pool + firstPool, firstGen, err := cpw.Get() + require.NoError(t, err) + require.NotNil(t, firstPool) + + // Create a second cert in SSL_CIR_DIR + certName := filepath.Join(tmpSslDir, "test2.pem") + t.Logf("Create cert file at %q\n", certName) + createCert(t, certName) + + require.Eventually(t, func() bool { + _, secondGen, err := cpw.Get() + if err != nil { + return false + } + // Because SSL_CERT_DIR is part of the SystemCertPool: + // 1. CPW only watches: it doesn't actually load it, that's the SystemCertPool's responsibility + // 2. Because the SystemCertPool never changes, we can't directly compare the pools + // 3. If SSL_CERT_DIR changes, we should expect a restart + return secondGen != firstGen && restarted.Load() + }, 10*time.Second, time.Second) +} + +func TestCertPoolWatcherSslCertFile(t *testing.T) { // create a temporary directory tmpDir, err := os.MkdirTemp("", "cert-pool") require.NoError(t, err) @@ -72,30 +166,78 @@ func TestCertPoolWatcher(t *testing.T) { t.Logf("Create cert file at %q\n", certName) createCert(t, certName) - // Update environment variables for the watcher - some of these should not exist - os.Setenv("SSL_CERT_DIR", tmpDir+":/tmp/does-not-exist.dir") - os.Setenv("SSL_CERT_FILE", "/tmp/does-not-exist.file") + // Update environment variables for the watcher + os.Unsetenv("SSL_CERT_DIR") + os.Setenv("SSL_CERT_FILE", certName) + defer os.Unsetenv("SSL_CERT_FILE") + + // Create a different CaDir + tmpCaDir := createTempCaDir(t) + defer os.RemoveAll(tmpCaDir) // Create the cert pool watcher - cpw, err := httputil.NewCertPoolWatcher(tmpDir, log.FromContext(context.Background())) + cpw, err := httputil.NewCertPoolWatcher(tmpCaDir, log.FromContext(context.Background())) require.NoError(t, err) + require.NotNil(t, cpw) defer cpw.Done() + restarted := &atomic.Bool{} + restarted.Store(false) + cpw.Restart(func(int) { restarted.Store(true) }) + err = cpw.Start(context.Background()) + require.NoError(t, err) // Get the original pool firstPool, firstGen, err := cpw.Get() require.NoError(t, err) require.NotNil(t, firstPool) - // Create a second cert - certName = filepath.Join(tmpDir, "test2.pem") + // Update the SSL_CERT_FILE t.Logf("Create cert file at %q\n", certName) createCert(t, certName) require.Eventually(t, func() bool { - secondPool, secondGen, err := cpw.Get() + _, secondGen, err := cpw.Get() if err != nil { return false } - return secondGen != firstGen && !firstPool.Equal(secondPool) - }, 30*time.Second, time.Second) + // Because SSL_CERT_FILE is part of the SystemCertPool: + // 1. CPW only watches: it doesn't actually load it, that's the SystemCertPool's responsibility + // 2. Because the SystemCertPool never changes, we can't directly compare the pools + // 3. If SSL_CERT_FILE changes, we should expect a restart + return secondGen != firstGen && restarted.Load() + }, 10*time.Second, time.Second) +} + +func TestCertPoolWatcherEmpty(t *testing.T) { + os.Unsetenv("SSL_CERT_FILE") + os.Unsetenv("SSL_CERT_DIR") + + // Create the empty cert pool watcher + cpw, err := httputil.NewCertPoolWatcher("", log.FromContext(context.Background())) + require.NoError(t, err) + require.NotNil(t, cpw) + defer cpw.Done() + err = cpw.Start(context.Background()) + require.NoError(t, err) + + pool, _, err := cpw.Get() + require.NoError(t, err) + require.NotNil(t, pool) +} + +func TestCertPoolInvalidPath(t *testing.T) { + os.Unsetenv("SSL_CERT_FILE") + os.Unsetenv("SSL_CERT_DIR") + + // Create an invalid cert pool watcher + cpw, err := httputil.NewCertPoolWatcher("/this/path/should/not/exist", log.FromContext(context.Background())) + require.NoError(t, err) + require.NotNil(t, cpw) + defer cpw.Done() + err = cpw.Start(context.Background()) + require.Error(t, err) + + pool, _, err := cpw.Get() + require.Error(t, err) + require.Nil(t, pool) } diff --git a/internal/shared/util/http/certutil.go b/internal/shared/util/http/certutil.go index fb7cdc4cbf..4dd83f0f49 100644 --- a/internal/shared/util/http/certutil.go +++ b/internal/shared/util/http/certutil.go @@ -35,7 +35,7 @@ func NewCertPool(caDir string, log logr.Logger) (*x509.CertPool, error) { log.V(defaultLogLevel).Info("skip directory", "name", e.Name()) continue } - log.V(defaultLogLevel).Info("load certificate", "name", e.Name(), "size", fi.Size(), "modtime", fi.ModTime()) + log.V(defaultLogLevel).Info("reading certificate file", "name", e.Name(), "size", fi.Size(), "modtime", fi.ModTime()) data, err := os.ReadFile(file) if err != nil { return nil, fmt.Errorf("error reading cert file %q: %w", file, err) @@ -44,7 +44,7 @@ func NewCertPool(caDir string, log logr.Logger) (*x509.CertPool, error) { if caCertPool.AppendCertsFromPEM(data) { count++ } - logPem(data, e.Name(), caDir, "loading certificate file", log) + logPem(data, e.Name(), caDir, "loading certificate", log) } // Found no certs! diff --git a/internal/shared/util/image/cache.go b/internal/shared/util/image/cache.go index fbbb52bd81..a82505ed5e 100644 --- a/internal/shared/util/image/cache.go +++ b/internal/shared/util/image/cache.go @@ -1,6 +1,7 @@ package image import ( + "bytes" "context" "errors" "fmt" @@ -10,22 +11,28 @@ import ( "os" "path/filepath" "slices" + "testing" "time" "github.com/containerd/containerd/archive" - "github.com/containers/image/v5/docker/reference" + "github.com/google/renameio/v2" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "go.podman.io/image/v5/docker/reference" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/registry" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" ) type LayerData struct { - Reader io.Reader - Index int - Err error + MediaType string + Reader io.Reader + Index int + Err error } type Cache interface { @@ -106,6 +113,40 @@ func (a *diskCache) unpackPath(ownerID string, digest digest.Digest) string { return filepath.Join(a.ownerIDPath(ownerID), digest.String()) } +type LayerUnpacker interface { + Unpack(_ context.Context, path string, layer LayerData, opts ...archive.ApplyOpt) error +} + +type defaultLayerUnpacker struct{} + +type chartLayerUnpacker struct{} + +var _ LayerUnpacker = &defaultLayerUnpacker{} +var _ LayerUnpacker = &chartLayerUnpacker{} + +func imageLayerUnpacker(layer LayerData) LayerUnpacker { + if features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) || testing.Testing() { + if layer.MediaType == registry.ChartLayerMediaType { + return &chartLayerUnpacker{} + } + } + return &defaultLayerUnpacker{} +} + +func (u *chartLayerUnpacker) Unpack(_ context.Context, path string, layer LayerData, _ ...archive.ApplyOpt) error { + if err := storeChartLayer(path, layer); err != nil { + return fmt.Errorf("error applying chart layer[%d]: %w", layer.Index, err) + } + return nil +} + +func (u *defaultLayerUnpacker) Unpack(ctx context.Context, path string, layer LayerData, opts ...archive.ApplyOpt) error { + if _, err := archive.Apply(ctx, path, layer.Reader, opts...); err != nil { + return fmt.Errorf("error applying layer[%d]: %w", layer.Index, err) + } + return nil +} + func (a *diskCache) Store(ctx context.Context, ownerID string, srcRef reference.Named, canonicalRef reference.Canonical, imgCfg ocispecv1.Image, layers iter.Seq[LayerData]) (fs.FS, time.Time, error) { var applyOpts []archive.ApplyOpt if a.filterFunc != nil { @@ -128,8 +169,9 @@ func (a *diskCache) Store(ctx context.Context, ownerID string, srcRef reference. if layer.Err != nil { return fmt.Errorf("error reading layer[%d]: %w", layer.Index, layer.Err) } - if _, err := archive.Apply(ctx, dest, layer.Reader, applyOpts...); err != nil { - return fmt.Errorf("error applying layer[%d]: %w", layer.Index, err) + layerUnpacker := imageLayerUnpacker(layer) + if err := layerUnpacker.Unpack(ctx, dest, layer, applyOpts...); err != nil { + return fmt.Errorf("unpacking layer: %w", err) } l.Info("applied layer", "layer", layer.Index) } @@ -147,6 +189,40 @@ func (a *diskCache) Store(ctx context.Context, ownerID string, srcRef reference. return os.DirFS(dest), modTime, nil } +func storeChartLayer(path string, layer LayerData) error { + if layer.Err != nil { + return fmt.Errorf("error found in layer data: %w", layer.Err) + } + data, err := io.ReadAll(layer.Reader) + if err != nil { + return fmt.Errorf("error reading layer[%d]: %w", layer.Index, err) + } + meta := new(chart.Metadata) + _, err = inspectChart(data, meta) + if err != nil { + return fmt.Errorf("inspecting chart layer: %w", err) + } + chart, err := renameio.TempFile("", + filepath.Join(path, + fmt.Sprintf("%s-%s.tgz", meta.Name, meta.Version), + ), + ) + if err != nil { + return fmt.Errorf("create temp file: %w", err) + } + defer func() { + _ = chart.Cleanup() + }() + if _, err := io.Copy(chart, bytes.NewReader(data)); err != nil { + return fmt.Errorf("copying chart archive: %w", err) + } + _, err = chart.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("seek chart archive start: %w", err) + } + return chart.CloseAtomicallyReplace() +} + func (a *diskCache) Delete(_ context.Context, ownerID string) error { return fsutil.DeleteReadOnlyRecursive(a.ownerIDPath(ownerID)) } diff --git a/internal/shared/util/image/cache_test.go b/internal/shared/util/image/cache_test.go index a5b644feb4..5f5e51b50d 100644 --- a/internal/shared/util/image/cache_test.go +++ b/internal/shared/util/image/cache_test.go @@ -2,8 +2,10 @@ package image import ( "archive/tar" + "bytes" "context" "errors" + "fmt" "io" "io/fs" "iter" @@ -16,10 +18,11 @@ import ( "time" "github.com/containerd/containerd/archive" - "github.com/containers/image/v5/docker/reference" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.podman.io/image/v5/docker/reference" + "helm.sh/helm/v3/pkg/registry" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" ) @@ -144,6 +147,67 @@ func TestDiskCacheFetch(t *testing.T) { } } +func TestDiskCacheStore_HelmChart(t *testing.T) { + const myOwner = "myOwner" + myCanonicalRef := mustParseCanonical(t, "my.registry.io/ns/chart@sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03") + myTaggedRef, err := reference.WithTag(reference.TrimNamed(myCanonicalRef), "test-tag") + require.NoError(t, err) + + testCases := []struct { + name string + ownerID string + srcRef reference.Named + canonicalRef reference.Canonical + imgConfig ocispecv1.Image + layers iter.Seq[LayerData] + filterFunc func(context.Context, reference.Named, ocispecv1.Image) (archive.Filter, error) + setup func(*testing.T, *diskCache) + expect func(*testing.T, *diskCache, fs.FS, time.Time, error) + }{ + { + name: "returns no error if layer read contains helm chart", + ownerID: myOwner, + srcRef: myTaggedRef, + canonicalRef: myCanonicalRef, + layers: func() iter.Seq[LayerData] { + testChart := mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + ) + return func(yield func(LayerData) bool) { + yield(LayerData{Reader: bytes.NewBuffer(testChart), MediaType: registry.ChartLayerMediaType}) + } + }(), + expect: func(t *testing.T, cache *diskCache, fsys fs.FS, modTime time.Time, err error) { + require.NoError(t, err) + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dc := &diskCache{ + basePath: t.TempDir(), + filterFunc: tc.filterFunc, + } + if tc.setup != nil { + tc.setup(t, dc) + } + fsys, modTime, err := dc.Store(context.Background(), tc.ownerID, tc.srcRef, tc.canonicalRef, tc.imgConfig, tc.layers) + require.NotNil(t, tc.expect, "test case must include an expect function") + tc.expect(t, dc, fsys, modTime, err) + require.NoError(t, fsutil.DeleteReadOnlyRecursive(dc.basePath)) + }) + } +} + func TestDiskCacheStore(t *testing.T) { const myOwner = "myOwner" myCanonicalRef := mustParseCanonical(t, "my.registry.io/ns/repo@sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03") @@ -585,6 +649,120 @@ func TestDiskCacheGarbageCollection(t *testing.T) { } } +func Test_storeChartLayer(t *testing.T) { + tmp := t.TempDir() + type args struct { + path string + data LayerData + } + type want struct { + errStr string + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "store chart layer to given path", + args: args{ + path: tmp, + data: LayerData{ + Index: 0, + MediaType: registry.ChartLayerMediaType, + Reader: bytes.NewBuffer(mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + )), + }, + }, + }, + { + name: "store invalid chart layer", + args: args{ + path: tmp, + data: LayerData{ + Index: 0, + MediaType: registry.ChartLayerMediaType, + Reader: bytes.NewBuffer(mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + )), + }, + }, + }, + { + name: "store existing from dummy reader", + args: args{ + path: tmp, + data: LayerData{ + Index: 0, + MediaType: registry.ChartLayerMediaType, + Reader: &dummyReader{}, + }, + }, + want: want{ + errStr: "error reading layer[0]: something went wrong", + }, + }, + { + name: "handle chart layer data", + args: args{ + path: tmp, + data: LayerData{ + Index: 0, + MediaType: registry.ChartLayerMediaType, + Err: fmt.Errorf("invalid layer data"), + Reader: bytes.NewBuffer(mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + )), + }, + }, + want: want{ + errStr: "error found in layer data: invalid layer data", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := storeChartLayer(tc.args.path, tc.args.data) + if tc.want.errStr != "" { + require.Error(t, err) + require.EqualError(t, err, tc.want.errStr, "chart store error") + } else { + require.NoError(t, err) + } + }) + } +} + func mustParseCanonical(t *testing.T, s string) reference.Canonical { n, err := reference.ParseNamed(s) require.NoError(t, err) @@ -619,3 +797,11 @@ func fsTarReader(fsys fs.FS) io.ReadCloser { }() return pr } + +type dummyReader struct{} + +var _ io.Reader = &dummyReader{} + +func (r *dummyReader) Read(p []byte) (int, error) { + return 0, errors.New("something went wrong") +} diff --git a/internal/shared/util/image/helm.go b/internal/shared/util/image/helm.go new file mode 100644 index 0000000000..299f921df5 --- /dev/null +++ b/internal/shared/util/image/helm.go @@ -0,0 +1,175 @@ +package image + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "iter" + "regexp" + "strings" + "time" + + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "go.podman.io/image/v5/docker/reference" + "go.podman.io/image/v5/pkg/blobinfocache/none" + "go.podman.io/image/v5/types" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/registry" +) + +func hasChart(imgCloser types.ImageCloser) bool { + config := imgCloser.ConfigInfo() + return config.MediaType == registry.ConfigMediaType +} + +func pullChart(ctx context.Context, ownerID string, srcRef reference.Named, canonicalRef reference.Canonical, imgSrc types.ImageSource, cache Cache) (fs.FS, time.Time, error) { + imgDigest := canonicalRef.Digest() + raw, _, err := imgSrc.GetManifest(ctx, &imgDigest) + if err != nil { + return nil, time.Time{}, fmt.Errorf("get OCI helm chart manifest; %w", err) + } + + chartManifest := ocispecv1.Manifest{} + if err := json.Unmarshal(raw, &chartManifest); err != nil { + return nil, time.Time{}, fmt.Errorf("unmarshaling chart manifest; %w", err) + } + + layerIter := iter.Seq[LayerData](func(yield func(LayerData) bool) { + for i, layer := range chartManifest.Layers { + ld := LayerData{Index: i, MediaType: layer.MediaType} + if layer.MediaType == registry.ChartLayerMediaType { + ld.Reader, _, ld.Err = imgSrc.GetBlob(ctx, + types.BlobInfo{ + Annotations: layer.Annotations, + MediaType: layer.MediaType, + Digest: layer.Digest, + Size: layer.Size, + }, + none.NoCache) + } + // Ignore the Helm provenance data layer + if layer.MediaType == registry.ProvLayerMediaType { + continue + } + if !yield(ld) { + return + } + } + }) + + return cache.Store(ctx, ownerID, srcRef, canonicalRef, ocispecv1.Image{}, layerIter) +} + +func IsValidChart(chart *chart.Chart) error { + if chart.Metadata == nil { + return errors.New("chart metadata is missing") + } + if chart.Metadata.Name == "" { + return errors.New("chart name is required") + } + if chart.Metadata.Version == "" { + return errors.New("chart version is required") + } + return chart.Metadata.Validate() +} + +type chartInspectionResult struct { + // templatesExist is set to true if the templates + // directory exists in the chart archive + templatesExist bool + // chartfileExists is set to true if the Chart.yaml + // file exists in the chart archive + chartfileExists bool +} + +func inspectChart(data []byte, metadata *chart.Metadata) (chartInspectionResult, error) { + report := chartInspectionResult{} + chart, err := loader.LoadArchive(bytes.NewBuffer(data)) + if err != nil { + return report, fmt.Errorf("loading chart archive: %w", err) + } + + report.templatesExist = len(chart.Templates) > 0 + report.chartfileExists = chart.Metadata != nil + + if metadata != nil && chart.Metadata != nil { + *metadata = *chart.Metadata + } + + return report, nil +} + +func IsBundleSourceChart(bundleFS fs.FS, metadata *chart.Metadata) (bool, error) { + var chartPath string + files, _ := fs.ReadDir(bundleFS, ".") + for _, file := range files { + if strings.HasSuffix(file.Name(), ".tgz") || + strings.HasSuffix(file.Name(), ".tar.gz") { + chartPath = file.Name() + break + } + } + + chartData, err := fs.ReadFile(bundleFS, chartPath) + if err != nil { + return false, err + } + + result, err := inspectChart(chartData, metadata) + if err != nil { + return false, fmt.Errorf("reading %s from fs: %w", chartPath, err) + } + + return (result.templatesExist && result.chartfileExists), nil +} + +type ChartOption func(*chart.Chart) + +func WithInstallNamespace(namespace string) ChartOption { + re := regexp.MustCompile(`{{\W+\.Release\.Namespace\W+}}`) + + return func(chrt *chart.Chart) { + for i, template := range chrt.Templates { + chrt.Templates[i].Data = re.ReplaceAll(template.Data, []byte(namespace)) + } + } +} + +func LoadChartFSWithOptions(bundleFS fs.FS, filename string, options ...ChartOption) (*chart.Chart, error) { + ch, err := loadChartFS(bundleFS, filename) + if err != nil { + return nil, err + } + + return enrichChart(ch, options...) +} + +func enrichChart(chart *chart.Chart, options ...ChartOption) (*chart.Chart, error) { + if chart == nil { + return nil, fmt.Errorf("chart can not be nil") + } + for _, f := range options { + f(chart) + } + return chart, nil +} + +var LoadChartFS = loadChartFS + +// loadChartFS loads a chart archive from a filesystem of +// type fs.FS with the provided filename +func loadChartFS(bundleFS fs.FS, filename string) (*chart.Chart, error) { + if filename == "" { + return nil, fmt.Errorf("chart file name was not provided") + } + + tarball, err := fs.ReadFile(bundleFS, filename) + if err != nil { + return nil, fmt.Errorf("reading chart %s; %+v", filename, err) + } + return loader.LoadArchive(bytes.NewBuffer(tarball)) +} diff --git a/internal/shared/util/image/helm_test.go b/internal/shared/util/image/helm_test.go new file mode 100644 index 0000000000..b47162f2e3 --- /dev/null +++ b/internal/shared/util/image/helm_test.go @@ -0,0 +1,682 @@ +package image + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "fmt" + "io/fs" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/containerd/containerd/archive" + goregistry "github.com/google/go-containerregistry/pkg/registry" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.podman.io/image/v5/docker" + "go.podman.io/image/v5/docker/reference" + "go.podman.io/image/v5/image" + "go.podman.io/image/v5/types" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/registry" + + fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" +) + +func Test_hasChart(t *testing.T) { + chartTagRef, _, cleanup := setupChartRegistry(t, + mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + ), + ) + defer cleanup() + + imgTagRef, _, shutdown := setupRegistry(t) + defer shutdown() + + type args struct { + srcRef string + contextFunc func(context.Context) (*types.SystemContext, error) + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "returns true when image contains chart", + args: args{ + srcRef: chartTagRef.String(), + contextFunc: buildSourceContextFunc(t, chartTagRef), + }, + want: true, + }, + { + name: "returns false when image is not chart", + args: args{ + srcRef: imgTagRef.String(), + contextFunc: buildSourceContextFunc(t, imgTagRef), + }, + want: false, + }, + } + + ctx := context.Background() + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + srcRef, err := reference.ParseNamed(tc.args.srcRef) + require.NoError(t, err) + + srcImgRef, err := docker.NewReference(srcRef) + require.NoError(t, err) + + sysCtx, err := tc.args.contextFunc(ctx) + require.NoError(t, err) + + imgSrc, err := srcImgRef.NewImageSource(ctx, sysCtx) + require.NoError(t, err) + + img, err := image.FromSource(ctx, sysCtx, imgSrc) + require.NoError(t, err) + + defer func() { + if err := img.Close(); err != nil { + panic(err) + } + }() + + got := hasChart(img) + require.Equal(t, tc.want, got) + }) + } +} + +func Test_pullChart(t *testing.T) { + const myOwner = "myOwner" + myChartName := "testchart-0.1.0.tgz" + testChart := mockHelmChartTgz(t, + []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + ) + + myTagRef, myCanonicalRef, cleanup := setupChartRegistry(t, testChart) + defer cleanup() + + tests := []struct { + name string + ownerID string + srcRef string + cache Cache + contextFunc func(context.Context) (*types.SystemContext, error) + expect func(*testing.T, fs.FS, time.Time) + }{ + { + name: "pull helm chart from OCI registry", + ownerID: myOwner, + srcRef: myTagRef.String(), + cache: &diskCache{ + basePath: t.TempDir(), + filterFunc: func(ctx context.Context, named reference.Named, image ocispecv1.Image) (archive.Filter, error) { + return forceOwnershipRWX(), nil + }, + }, + contextFunc: buildSourceContextFunc(t, myTagRef), + expect: func(t *testing.T, fsys fs.FS, modTime time.Time) { + now := time.Now() + require.LessOrEqual(t, now.Sub(modTime), 3*time.Second, "modified time should less than 3 seconds") + + actualChartData, err := fs.ReadFile(fsys, myChartName) + require.NoError(t, err) + + assert.Equal(t, testChart, actualChartData) + }, + }, + } + + ctx := context.Background() + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + srcRef, err := reference.ParseNamed(tc.srcRef) + require.NoError(t, err) + + srcImgRef, err := docker.NewReference(srcRef) + require.NoError(t, err) + + sysCtx, err := tc.contextFunc(ctx) + require.NoError(t, err) + + imgSrc, err := srcImgRef.NewImageSource(ctx, sysCtx) + require.NoError(t, err) + + fsys, modTime, err := pullChart(ctx, tc.ownerID, srcRef, myCanonicalRef, imgSrc, tc.cache) + require.NotNil(t, tc.expect, "expect function must be defined") + require.NoError(t, err) + + tc.expect(t, fsys, modTime) + + if dc, ok := tc.cache.(*diskCache); ok && dc.basePath != "" { + require.NoError(t, fsutil.DeleteReadOnlyRecursive(dc.basePath)) + } + }) + } +} + +func TestIsValidChart(t *testing.T) { + tt := []struct { + name string + target *chart.Chart + wantErr bool + errMsg string + }{ + { + name: "helm chart with required metadata", + target: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "sample-chart", + Version: "0.1.2", + }, + }, + wantErr: false, + }, + { + name: "helm chart without name", + target: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "", + Version: "0.1.2", + }, + }, + wantErr: true, + errMsg: "chart name is required", + }, + { + name: "helm chart with missing version", + target: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "sample-chart", + Version: "", + }, + }, + wantErr: true, + errMsg: "chart version is required", + }, + { + name: "helm chart with missing metadata", + target: &chart.Chart{ + Metadata: nil, + }, + wantErr: true, + errMsg: "chart metadata is missing", + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + err := IsValidChart(tc.target) + if tc.wantErr && assert.Error(t, err, "checking valid chart") { + assert.EqualError(t, err, tc.errMsg, "validating chart") + } + }) + } +} + +func TestIsBundleSourceChart(t *testing.T) { + type args struct { + meta *chart.Metadata + files []fileContent + } + type want struct { + value bool + errStr string + } + tt := []struct { + name string + args args + want want + }{ + { + name: "complete helm chart with nil *chart.Metadata", + args: args{ + meta: nil, + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + value: true, + }, + }, + { + name: "complete helm chart", + args: args{ + meta: &chart.Metadata{}, + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + value: true, + }, + }, + { + name: "helm chart without templates", + args: args{ + meta: nil, + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + }, + }, + want: want{ + value: false, + }, + }, + { + name: "helm chart without a Chart.yaml", + args: args{ + meta: nil, + files: []fileContent{ + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + value: false, + errStr: "reading testchart-0.1.0.tgz from fs: loading chart archive: Chart.yaml file is missing", + }, + }, + { + name: "invalid chart archive", + args: args{ + meta: nil, + files: []fileContent{ + { + name: "testchart/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + value: false, + errStr: "reading testchart-0.1.0.tgz from fs: loading chart archive: Chart.yaml file is missing", + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + chartFS, _ := createTempFS(t, mockHelmChartTgz(t, tc.args.files)) + got, err := IsBundleSourceChart(chartFS, tc.args.meta) + if tc.want.errStr != "" { + require.Error(t, err, "chart validation error required") + require.EqualError(t, err, tc.want.errStr, "chart error") + } + require.Equal(t, tc.want.value, got, "validata helm chart") + }) + } +} + +func Test_loadChartFS(t *testing.T) { + type args struct { + filename string + files []fileContent + } + type want struct { + name string + version string + errMsg string + } + tests := []struct { + name string + args args + want want + expect func(*chart.Chart, want, error) + }{ + { + name: "empty filename is provided", + args: args{ + filename: "", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + name: "", + errMsg: "chart file name was not provided", + }, + expect: func(chart *chart.Chart, want want, err error) { + require.EqualError(t, err, want.errMsg) + assert.Nil(t, chart, "no chart would be returned") + }, + }, + { + name: "load sample chart", + args: args{ + filename: "testchart-0.1.0.tgz", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + name: "testchart", + version: "0.1.0", + }, + expect: func(chart *chart.Chart, want want, err error) { + require.NoError(t, err, "chart should load successfully") + assert.Equal(t, want.name, chart.Metadata.Name, "verify chart name") + assert.Equal(t, want.version, chart.Metadata.Version, "verify chart version") + }, + }, + { + name: "load nonexistent chart", + args: args{ + filename: "nonexistent-chart-0.1.0.tgz", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + name: "nonexistent-chart", + version: "0.1.0", + }, + expect: func(chart *chart.Chart, want want, err error) { + assert.Nil(t, chart, "chart does not exist on filesystem") + require.Error(t, err, "reading chart nonexistent-chart-0.1.0.tgz; open nonexistent-chart-0.1.0.tgz: no such file or directory") + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + chartFS, _ := createTempFS(t, mockHelmChartTgz(t, tc.args.files)) + + got, err := loadChartFS(chartFS, tc.args.filename) + assert.NotNil(t, tc.expect, "validation function") + tc.expect(got, tc.want, err) + }) + } +} + +func TestLoadChartFSWithOptions(t *testing.T) { + type args struct { + filename string + files []fileContent + } + type want struct { + name string + version string + errMsg string + } + tests := []struct { + name string + args args + want want + expect func(*chart.Chart, want, error) + }{ + { + name: "empty filename is provided", + args: args{ + filename: "", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + errMsg: "chart file name was not provided", + }, + expect: func(chart *chart.Chart, want want, err error) { + require.Error(t, err, want.errMsg) + }, + }, + { + name: "load sample chart", + args: args{ + filename: "testchart-0.1.0.tgz", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + name: "testchart", + version: "0.1.0", + }, + expect: func(chart *chart.Chart, want want, err error) { + require.NoError(t, err) + assert.Equal(t, want.name, chart.Metadata.Name, "chart name") + assert.Equal(t, want.version, chart.Metadata.Version, "chart version") + }, + }, + { + name: "load nonexistent chart", + args: args{ + filename: "nonexistent-chart-0.1.0.tgz", + files: []fileContent{ + { + name: "testchart/Chart.yaml", + content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"), + }, + { + name: "testchart/templates/deployment.yaml", + content: []byte("kind: Deployment\napiVersion: apps/v1"), + }, + }, + }, + want: want{ + errMsg: "reading chart nonexistent-chart-0.1.0.tgz; open nonexistent-chart-0.1.0.tgz: no such file or directory", + }, + expect: func(chart *chart.Chart, want want, err error) { + require.Error(t, err, want.errMsg) + assert.Nil(t, chart) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + chartFS, _ := createTempFS(t, mockHelmChartTgz(t, tc.args.files)) + got, err := LoadChartFSWithOptions(chartFS, tc.args.filename, WithInstallNamespace("metrics-server-system")) + require.NotNil(t, tc.expect) + tc.expect(got, tc.want, err) + }) + } +} + +func Test_enrichChart(t *testing.T) { + type args struct { + chart *chart.Chart + options []ChartOption + } + tests := []struct { + name string + args args + want *chart.Chart + wantErr bool + }{ + { + name: "enrich empty chart object", + args: args{ + chart: nil, + options: []ChartOption{ + WithInstallNamespace("test-namespace-system"), + }, + }, + wantErr: true, + want: nil, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := enrichChart(tc.args.chart, tc.args.options...) + if (err != nil) != tc.wantErr { + t.Errorf("enrichChart() error = %v, wantErr %v", err, tc.wantErr) + return + } + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("enrichChart() = %v, want %v", got, tc.want) + } + }) + } +} + +func setupChartRegistry(t *testing.T, chart []byte) (reference.NamedTagged, reference.Canonical, func()) { + server := httptest.NewServer(goregistry.New()) + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + clientOpts := []registry.ClientOption{ + registry.ClientOptDebug(true), + registry.ClientOptEnableCache(true), + } + client, err := registry.NewClient(clientOpts...) + require.NoError(t, err) + + testCreationTime := "2020-09-22T22:04:05Z" + ref := fmt.Sprintf("%s/testrepo/testchart:%s", serverURL.Host, "0.1.0") + result, err := client.Push(chart, ref, registry.PushOptCreationTime(testCreationTime)) + require.NoError(t, err) + + imageTagRef, err := newReference(serverURL.Host, "testrepo/testchart", "0.1.0") + require.NoError(t, err) + + imageDigestRef, err := reference.WithDigest( + reference.TrimNamed(imageTagRef), + digest.Digest(result.Manifest.Digest), + ) + require.NoError(t, err) + + return imageTagRef, imageDigestRef, func() { + server.Close() + } +} + +type fileContent struct { + name string + content []byte +} + +func mockHelmChartTgz(t *testing.T, contents []fileContent) []byte { + require.NotEmpty(t, contents, "chart content required") + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + + // Add files to the chart archive + for _, file := range contents { + require.NoError(t, tw.WriteHeader(&tar.Header{ + Name: file.name, + Mode: 0600, + Size: int64(len(file.content)), + })) + _, _ = tw.Write(file.content) + } + + require.NoError(t, tw.Close()) + + var gzBuf bytes.Buffer + gz := gzip.NewWriter(&gzBuf) + _, err := gz.Write(buf.Bytes()) + require.NoError(t, err) + require.NoError(t, gz.Close()) + + return gzBuf.Bytes() +} + +func createTempFS(t *testing.T, data []byte) (fs.FS, error) { + require.NotEmpty(t, data, "chart data") + tmpDir, _ := os.MkdirTemp(t.TempDir(), "bundlefs-") + if len(data) == 0 { + return os.DirFS(tmpDir), nil + } + + dest, err := os.Create(filepath.Join(tmpDir, "testchart-0.1.0.tgz")) + if err != nil { + return nil, err + } + defer dest.Close() + + if _, err := dest.Write(data); err != nil { + return nil, err + } + + return os.DirFS(tmpDir), nil +} diff --git a/internal/shared/util/image/mocks.go b/internal/shared/util/image/mocks.go index 9039831818..82e8226e26 100644 --- a/internal/shared/util/image/mocks.go +++ b/internal/shared/util/image/mocks.go @@ -6,8 +6,8 @@ import ( "iter" "time" - "github.com/containers/image/v5/docker/reference" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "go.podman.io/image/v5/docker/reference" ) var _ Puller = (*MockPuller)(nil) diff --git a/internal/shared/util/image/pull.go b/internal/shared/util/image/pull.go index cbef0dcd7c..1fff90809b 100644 --- a/internal/shared/util/image/pull.go +++ b/internal/shared/util/image/pull.go @@ -9,21 +9,22 @@ import ( "os" "time" - "github.com/containers/image/v5/copy" - "github.com/containers/image/v5/docker" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/image" - "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/oci/layout" - "github.com/containers/image/v5/pkg/blobinfocache/none" - "github.com/containers/image/v5/pkg/compression" - "github.com/containers/image/v5/pkg/sysregistriesv2" - "github.com/containers/image/v5/signature" - "github.com/containers/image/v5/types" "github.com/go-logr/logr" + "go.podman.io/image/v5/copy" + "go.podman.io/image/v5/docker" + "go.podman.io/image/v5/docker/reference" + "go.podman.io/image/v5/image" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/oci/layout" + "go.podman.io/image/v5/pkg/blobinfocache/none" + "go.podman.io/image/v5/pkg/compression" + "go.podman.io/image/v5/pkg/sysregistriesv2" + "go.podman.io/image/v5/signature" + "go.podman.io/image/v5/types" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/shared/util/http" ) @@ -224,6 +225,12 @@ func (p *ContainersImagePuller) applyImage(ctx context.Context, ownerID string, } }() + if features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) { + if hasChart(img) { + return pullChart(ctx, ownerID, srcRef, canonicalRef, imgSrc, cache) + } + } + ociImg, err := img.OCIConfig(ctx) if err != nil { return nil, time.Time{}, err @@ -231,7 +238,7 @@ func (p *ContainersImagePuller) applyImage(ctx context.Context, ownerID string, layerIter := iter.Seq[LayerData](func(yield func(LayerData) bool) { for i, layerInfo := range img.LayerInfos() { - ld := LayerData{Index: i} + ld := LayerData{Index: i, MediaType: layerInfo.MediaType} layerReader, _, err := imgSrc.GetBlob(ctx, layerInfo, none.NoCache) if err != nil { ld.Err = fmt.Errorf("error getting layer blob reader: %w", err) diff --git a/internal/shared/util/image/pull_test.go b/internal/shared/util/image/pull_test.go index 5aca3d75e9..45a04062f5 100644 --- a/internal/shared/util/image/pull_test.go +++ b/internal/shared/util/image/pull_test.go @@ -15,15 +15,15 @@ import ( "github.com/BurntSushi/toml" "github.com/containerd/containerd/archive" - "github.com/containers/image/v5/docker/reference" - "github.com/containers/image/v5/pkg/sysregistriesv2" - "github.com/containers/image/v5/types" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/registry" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.podman.io/image/v5/docker/reference" + "go.podman.io/image/v5/pkg/sysregistriesv2" + "go.podman.io/image/v5/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" diff --git a/internal/shared/util/pullsecretcache/pullsecretcache.go b/internal/shared/util/pullsecretcache/pullsecretcache.go new file mode 100644 index 0000000000..910b4b50b8 --- /dev/null +++ b/internal/shared/util/pullsecretcache/pullsecretcache.go @@ -0,0 +1,40 @@ +package pullsecretcache + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/cache" +) + +func SetupPullSecretCache(cacheOptions *cache.Options, globalPullSecretKey *types.NamespacedName, saKey types.NamespacedName) error { + cacheOptions.ByObject[&corev1.ServiceAccount{}] = cache.ByObject{ + Namespaces: map[string]cache.Config{ + saKey.Namespace: { + LabelSelector: labels.Everything(), + FieldSelector: fields.SelectorFromSet(map[string]string{ + "metadata.name": saKey.Name, + }), + }, + }, + } + + secretCache := cache.ByObject{} + secretCache.Namespaces = make(map[string]cache.Config, 2) + secretCache.Namespaces[saKey.Namespace] = cache.Config{ + LabelSelector: labels.Everything(), + FieldSelector: fields.Everything(), + } + if globalPullSecretKey != nil && globalPullSecretKey.Namespace != saKey.Namespace { + secretCache.Namespaces[globalPullSecretKey.Namespace] = cache.Config{ + LabelSelector: labels.Everything(), + FieldSelector: fields.SelectorFromSet(map[string]string{ + "metadata.name": globalPullSecretKey.Name, + }), + } + } + cacheOptions.ByObject[&corev1.Secret{}] = secretCache + + return nil +} diff --git a/internal/shared/util/sa/serviceaccount.go b/internal/shared/util/sa/serviceaccount.go new file mode 100644 index 0000000000..f668c859e8 --- /dev/null +++ b/internal/shared/util/sa/serviceaccount.go @@ -0,0 +1,51 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sa + +import ( + "fmt" + "os" + "strings" + + "github.com/golang-jwt/jwt/v5" + k8stypes "k8s.io/apimachinery/pkg/types" +) + +// Returns nameaspce/serviceaccount name +func GetServiceAccount() (k8stypes.NamespacedName, error) { + return getServiceAccountInternal(os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")) +} + +func getServiceAccountInternal(data []byte, err error) (k8stypes.NamespacedName, error) { + if err != nil { + return k8stypes.NamespacedName{}, err + } + // Not verifying the token, we just want to extract the subject + token, _, err := jwt.NewParser([]jwt.ParserOption{}...).ParseUnverified(string(data), jwt.MapClaims{}) + if err != nil { + return k8stypes.NamespacedName{}, err + } + subject, err := token.Claims.GetSubject() + if err != nil { + return k8stypes.NamespacedName{}, err + } + subjects := strings.Split(subject, ":") + if len(subjects) != 4 || subjects[2] == "" || subjects[3] == "" { + return k8stypes.NamespacedName{}, fmt.Errorf("badly formatted subject: %s", subject) + } + return k8stypes.NamespacedName{Namespace: subjects[2], Name: subjects[3]}, nil +} diff --git a/internal/shared/util/sa/serviceaccount_test.go b/internal/shared/util/sa/serviceaccount_test.go new file mode 100644 index 0000000000..b18663e662 --- /dev/null +++ b/internal/shared/util/sa/serviceaccount_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sa + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + // taken from a kind run + goodSa = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b2xtdjEtc3lzdGVtOm9wZXJhdG9yLWNvbnRyb2xsZXItY29udHJvbGxlci1tYW5hZ2VyIn0.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA" // notsecret + badSa1 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnRzOm9sbXYxLXN5c3RlbSJ9.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA" // notsecret + badSa2 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ" // notsecret +) + +func TestGetServiceAccount(t *testing.T) { + nn, err := getServiceAccountInternal([]byte(goodSa), nil) + require.NoError(t, err) + require.Equal(t, "olmv1-system", nn.Namespace) + require.Equal(t, "operator-controller-controller-manager", nn.Name) + + _, err = getServiceAccountInternal([]byte{}, fmt.Errorf("this is a test error")) + require.ErrorContains(t, err, "this is a test") + + // Modified the subject to be invalid + _, err = getServiceAccountInternal([]byte(badSa1), nil) + require.ErrorContains(t, err, "badly formatted subject") + + // Only includes a header + _, err = getServiceAccountInternal([]byte(badSa2), nil) + require.ErrorContains(t, err, "token is malformed") +} diff --git a/test/utils/artifacts.go b/internal/shared/util/testutils/artifacts.go similarity index 77% rename from test/utils/artifacts.go rename to internal/shared/util/testutils/artifacts.go index acb523adef..6bda44b673 100644 --- a/test/utils/artifacts.go +++ b/internal/shared/util/testutils/artifacts.go @@ -1,4 +1,4 @@ -package utils +package testutils import ( "context" @@ -11,13 +11,13 @@ import ( "time" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" kubeclient "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/utils/env" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" ocv1 "github.com/operator-framework/operator-controller/api/v1" ) @@ -25,6 +25,7 @@ import ( // CollectTestArtifacts gets all the artifacts from the test run and saves them to the artifact path. // Currently, it saves: // - clusterextensions +// - clusterextensionrevisions // - pods logs // - deployments // - catalogsources @@ -54,7 +55,7 @@ func CollectTestArtifacts(t *testing.T, artifactName string, c client.Client, cf // get all cluster extensions save them to the artifact path. clusterExtensions := ocv1.ClusterExtensionList{} - if err := c.List(context.Background(), &clusterExtensions, client.InNamespace("")); err != nil { + if err := c.List(context.Background(), &clusterExtensions); err != nil { fmt.Printf("Failed to list cluster extensions: %v", err) } for _, clusterExtension := range clusterExtensions.Items { @@ -69,6 +70,23 @@ func CollectTestArtifacts(t *testing.T, artifactName string, c client.Client, cf } } + // get all cluster extension revisions save them to the artifact path. + clusterExtensionRevisions := ocv1.ClusterExtensionRevisionList{} + if err := c.List(context.Background(), &clusterExtensionRevisions); err != nil { + fmt.Printf("Failed to list cluster extensions: %v", err) + } + for _, cer := range clusterExtensionRevisions.Items { + // Save cluster extension to artifact path + clusterExtensionYaml, err := yaml.Marshal(cer) + if err != nil { + fmt.Printf("Failed to marshal cluster extension: %v", err) + continue + } + if err := os.WriteFile(filepath.Join(artifactPath, cer.Name+"-clusterextensionrevision.yaml"), clusterExtensionYaml, 0600); err != nil { + fmt.Printf("Failed to write cluster extension to file: %v", err) + } + } + // get all catalogsources save them to the artifact path. catalogsources := ocv1.ClusterCatalogList{} if err := c.List(context.Background(), &catalogsources, client.InNamespace("")); err != nil { @@ -117,6 +135,23 @@ func CollectTestArtifacts(t *testing.T, artifactName string, c client.Client, cf } } + // Get secrets in all namespaces + secrets := corev1.SecretList{} + if err := c.List(context.Background(), &secrets, client.InNamespace(namespace.Name)); err != nil { + fmt.Printf("Failed to list secrets %v in namespace: %q", err, namespace.Name) + } + for _, secret := range secrets.Items { + // Save secret to artifact path + secretYaml, err := yaml.Marshal(secret) + if err != nil { + fmt.Printf("Failed to marshal secret: %v", err) + continue + } + if err := os.WriteFile(filepath.Join(namespacedArtifactPath, secret.Name+"-secret.yaml"), secretYaml, 0600); err != nil { + fmt.Printf("Failed to write secret to file: %v", err) + } + } + // Get logs from all pods in all namespaces pods := corev1.PodList{} if err := c.List(context.Background(), &pods, client.InNamespace(namespace.Name)); err != nil { diff --git a/internal/shared/util/testutils/summary.go b/internal/shared/util/testutils/summary.go new file mode 100644 index 0000000000..37c1d51e01 --- /dev/null +++ b/internal/shared/util/testutils/summary.go @@ -0,0 +1,209 @@ +package testutils + +import ( + "context" + "fmt" + "math" + "os" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/prometheus/client_golang/api" + v1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" +) + +var ( + summaryTemplate = "summary.md.tmpl" + alertsTemplate = "alert.md.tmpl" + chartTemplate = "mermaid_chart.md.tmpl" + defaultPromUrl = "http://localhost:30900/" +) + +type summaryAlerts struct { + FiringAlerts []summaryAlert + PendingAlerts []summaryAlert +} + +type summaryAlert struct { + v1.Alert + Name string + Description string +} + +type xychart struct { + Title string + YMax float64 + YMin float64 + YLabel string + Data string +} + +type githubSummary struct { + client api.Client + Pods []string + alertsFiring bool +} + +func NewSummary(c api.Client, pods ...string) githubSummary { + return githubSummary{ + client: c, + Pods: pods, + alertsFiring: false, + } +} + +// PerformanceQuery queries the prometheus server and generates a mermaid xychart with the data. +// title - Display name of the xychart +// pod - Pod name with which to filter results from prometheus +// query - Prometheus query +// yLabel - Label of the Y axis i.e. "KB/s", "MB", etc. +// scaler - Constant by which to scale the results. For instance, cpu usage is more human-readable +// as "mCPU" vs "CPU", so we scale the results by a factor of 1,000. +func (s *githubSummary) PerformanceQuery(title, pod, query, yLabel string, scaler float64) (string, error) { + v1api := v1.NewAPI(s.client) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + fullQuery := fmt.Sprintf(query, pod) + result, warnings, err := v1api.Query(ctx, fullQuery, time.Now()) + if err != nil { + return "", err + } else if len(warnings) > 0 { + fmt.Printf("warnings returned from performance query; query=%s, warnings=%v", fullQuery, warnings) + } else if result.Type() != model.ValMatrix { + return "", fmt.Errorf("incompatible result type; need: %s, got: %s", model.ValMatrix, result.Type().String()) + } + + matrix, ok := result.(model.Matrix) + if !ok { + return "", fmt.Errorf("typecast for metrics samples failed; aborting") + } else if len(matrix) > 1 { + return "", fmt.Errorf("expected 1 set of results; got: %d", len(matrix)) + } + chart := xychart{ + Title: title, + YLabel: yLabel, + YMax: math.SmallestNonzeroFloat64, + YMin: math.MaxFloat64, + } + formattedData := make([]string, 0) + // matrix does not allow [] access, so we just do one iteration for the single result + for _, metric := range matrix { + if len(metric.Values) < 2 { + // A graph with one data point means something with the collection was wrong + return "", fmt.Errorf("expected at least two data points; got: %d", len(metric.Values)) + } + for _, sample := range metric.Values { + floatSample := float64(sample.Value) * scaler + formattedData = append(formattedData, fmt.Sprintf("%f", floatSample)) + if floatSample > chart.YMax { + chart.YMax = floatSample + } + if floatSample < chart.YMin { + chart.YMin = floatSample + } + } + } + // Add some padding + chart.YMax = (chart.YMax + (math.Abs(chart.YMax) * 0.05)) + chart.YMin = (chart.YMin - (math.Abs(chart.YMin) * 0.05)) + // Pretty print the values, ex: [1,2,3,4] + chart.Data = strings.ReplaceAll(fmt.Sprintf("%v", formattedData), " ", ",") + + return executeTemplate(chartTemplate, chart) +} + +// Alerts queries the prometheus server for alerts and generates markdown output for anything found. +// If no alerts are found, the alerts section will contain only "None." in the final output. +func (s *githubSummary) Alerts() (string, error) { + v1api := v1.NewAPI(s.client) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + result, err := v1api.Alerts(ctx) + if err != nil { + return "", err + } + + firingAlerts := make([]summaryAlert, 0) + pendingAlerts := make([]summaryAlert, 0) + if len(result.Alerts) > 0 { + for _, a := range result.Alerts { + aConv := summaryAlert{ + Alert: a, + Name: string(a.Labels["alertname"]), + Description: string(a.Annotations["description"]), + } + switch a.State { + case v1.AlertStateFiring: + firingAlerts = append(firingAlerts, aConv) + s.alertsFiring = true + case v1.AlertStatePending: + pendingAlerts = append(pendingAlerts, aConv) + // Ignore AlertStateInactive; the alerts endpoint doesn't return them + } + } + } else { + return "None.", nil + } + + return executeTemplate(alertsTemplate, summaryAlerts{ + FiringAlerts: firingAlerts, + PendingAlerts: pendingAlerts, + }) +} + +func executeTemplate(templateFile string, obj any) (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get working directory: %w", err) + } + tmpl, err := template.New(templateFile).ParseGlob(filepath.Join(wd, "../../internal/shared/util/testutils/templates", templateFile)) + if err != nil { + return "", err + } + buffer := new(strings.Builder) + err = tmpl.Execute(buffer, obj) + if err != nil { + return "", err + } + return buffer.String(), nil +} + +// PrintSummary executes the main summary template, generating the full test report. +// The markdown is template-driven; the summary methods are called from within the +// template. This allows us to add or change queries (hopefully) without needing to +// touch code. The summary will be output to a file supplied by the env target. +func PrintSummary(path string) error { + if path == "" { + fmt.Printf("No summary output path specified; skipping") + return nil + } + + client, err := api.NewClient(api.Config{ + Address: defaultPromUrl, + }) + if err != nil { + fmt.Printf("warning: failed to initialize promQL client: %v", err) + return nil + } + + summary := NewSummary(client, "operator-controller", "catalogd") + summaryMarkdown, err := executeTemplate(summaryTemplate, &summary) + if err != nil { + fmt.Printf("warning: failed to generate e2e test summary: %v", err) + return nil + } + err = os.WriteFile(path, []byte(summaryMarkdown), 0o600) + if err != nil { + fmt.Printf("warning: failed to write e2e test summary output to %s: %v", path, err) + return nil + } + fmt.Printf("Test summary output to %s successful\n", path) + if summary.alertsFiring { + return fmt.Errorf("performance alerts encountered during test run; please check e2e test summary for details") + } + return nil +} diff --git a/internal/shared/util/testutils/templates/alert.md.tmpl b/internal/shared/util/testutils/templates/alert.md.tmpl new file mode 100644 index 0000000000..39f3e42879 --- /dev/null +++ b/internal/shared/util/testutils/templates/alert.md.tmpl @@ -0,0 +1,16 @@ +{{- /* -------------------- Alert Template --------------------- */ -}} +{{define "alert"}} +| {{ .Name }} | {{ .Description }} | +| -------- | ------- | +| ActiveAt | {{ .ActiveAt }} | +| State | {{ .State }} | +{{- end}} + +### Firing Alerts +{{ range .FiringAlerts }} +{{ template "alert" .}} +{{ end }} +### Pending Alerts +{{ range .PendingAlerts }} +{{ template "alert" .}} +{{ end }} diff --git a/internal/shared/util/testutils/templates/mermaid_chart.md.tmpl b/internal/shared/util/testutils/templates/mermaid_chart.md.tmpl new file mode 100644 index 0000000000..0a8ed1135b --- /dev/null +++ b/internal/shared/util/testutils/templates/mermaid_chart.md.tmpl @@ -0,0 +1,17 @@ +
+ +```mermaid +--- +config: + xyChart: + showDataLabel: true + xAxis: + showLabel: false +--- +xychart-beta +title "{{ .Title }}" +y-axis "{{ .YLabel }}" {{printf "%f" .YMin}} --> {{printf "%f" .YMax}} +x-axis "time (start of test to end)" +line {{.Data}} +``` +
diff --git a/internal/shared/util/testutils/templates/summary.md.tmpl b/internal/shared/util/testutils/templates/summary.md.tmpl new file mode 100644 index 0000000000..b1372b8740 --- /dev/null +++ b/internal/shared/util/testutils/templates/summary.md.tmpl @@ -0,0 +1,29 @@ + +{{- /* ------------ Performance Statistics Template ------------ */ -}} +{{define "performanceStatistics" -}} +{{ range $index, $pod := .Pods }} +### {{$pod}} +#### Memory Usage +{{$.PerformanceQuery "Memory Usage" $pod `container_memory_working_set_bytes{pod=~"%s.*",container="manager"}[5m]` "MB" .000001}} + +#### Memory Growth Rate +{{$.PerformanceQuery "Memory Growth Rate" $pod `deriv(sum(container_memory_working_set_bytes{pod=~"%s.*",container="manager"})[5m:])[5m:]` "KB/s" .001}} + +#### CPU Usage +{{$.PerformanceQuery "CPU Usage" $pod `rate(container_cpu_usage_seconds_total{pod=~"%s.*",container="manager"}[5m])[5m:]` "mCPU" 1000}} + +#### API Queries Total +{{$.PerformanceQuery "API Queries Total" $pod `sum(rest_client_requests_total{job=~"%s.*"})[5m:]` "# queries" 1}} + +#### API Query Rate +{{$.PerformanceQuery "API Queries/sec" $pod `sum(rate(rest_client_requests_total{job=~"%s.*"}[5m]))[5m:]` "per sec" 1}} + +{{end}} +{{- end}} + +{{- /* ----------------- E2E Summary Markdown ------------------ */ -}} +# E2E Summary +## Alerts +{{.Alerts}} +## Performance +{{ template "performanceStatistics" . -}} diff --git a/internal/shared/util/testutils/utils.go b/internal/shared/util/testutils/utils.go new file mode 100644 index 0000000000..94eb2d5b3c --- /dev/null +++ b/internal/shared/util/testutils/utils.go @@ -0,0 +1,23 @@ +package testutils + +import ( + "os/exec" + "testing" +) + +// FindK8sClient returns the first available Kubernetes CLI client from the system, +// It checks for the existence of each client by running `version --client`. +// If no suitable client is found, the function terminates the test with a failure. +func FindK8sClient(t *testing.T) string { + t.Logf("Finding kubectl client") + clients := []string{"kubectl", "oc"} + for _, c := range clients { + // Would prefer to use `command -v`, but even that may not be installed! + if err := exec.Command(c, "version", "--client").Run(); err == nil { + t.Logf("Using %q as k8s client", c) + return c + } + } + t.Fatal("k8s client not found") + return "" +} diff --git a/internal/shared/util/tlsprofiles/flags.go b/internal/shared/util/tlsprofiles/flags.go new file mode 100644 index 0000000000..682963b497 --- /dev/null +++ b/internal/shared/util/tlsprofiles/flags.go @@ -0,0 +1,189 @@ +package tlsprofiles + +import ( + "bytes" + "crypto/tls" + "encoding/csv" + "fmt" + "maps" + "slices" + "strings" + + "github.com/spf13/pflag" +) + +func AddFlags(fs *pflag.FlagSet) { + fs.Var(&configuredProfile, "tls-profile", "The TLS profile to use. One of "+fmt.Sprintf("%v", slices.Sorted(maps.Keys(profiles)))) + fs.Var(&customTLSProfile.ciphers, "tls-custom-ciphers", "List of ciphers to be used with the custom TLS profile. Use Go-language cipher names") + fs.Var(&customTLSProfile.curves, "tls-custom-curves", "List of curves to be used with the custom TLS profile. Values may consist of "+fmt.Sprintf("%v", slices.Sorted(maps.Keys(curves)))) + fs.Var(&customTLSProfile.minTLSVersion, "tls-custom-version", "The TLS version to be used with the custom TLS profile. One of "+fmt.Sprintf("%v", slices.Sorted(maps.Keys(tlsVersions)))) +} + +// Definition of the type for `--tls-profile` +type tlsProfileName string + +func (p *tlsProfileName) String() string { + return string(*p) +} + +func (p *tlsProfileName) Type() string { + return "string" +} + +func (p *tlsProfileName) Set(value string) error { + newValue := tlsProfileName(value) + _, err := findTLSProfile(newValue) + if err != nil { + return err + } + *p = newValue + return nil +} + +// Definition of the type for `--tls-custom-ciphers` +type cipherSlice struct { + cipherNums []uint16 + cipherNames []string +} + +func readAsCSV(val string) ([]string, error) { + if val == "" { + return []string{}, nil + } + stringReader := strings.NewReader(val) + csvReader := csv.NewReader(stringReader) + return csvReader.Read() +} + +func writeAsCSV(vals []string) (string, error) { + b := &bytes.Buffer{} + w := csv.NewWriter(b) + err := w.Write(vals) + if err != nil { + return "", err + } + w.Flush() + return strings.TrimSuffix(b.String(), "\n"), nil +} + +func (s *cipherSlice) Set(val string) error { + v, err := readAsCSV(val) + if err != nil { + return err + } + return s.Replace(v) +} + +func (s *cipherSlice) Type() string { + return "stringSlice" +} + +func (s *cipherSlice) String() string { + str, _ := writeAsCSV(s.cipherNames) + return "[" + str + "]" +} + +func (s *cipherSlice) Append(val string) error { + num := cipherSuiteId(val) + if num == 0 { + return fmt.Errorf("unknown cipher %q", val) + } + s.cipherNums = append(s.cipherNums, num) + s.cipherNames = append(s.cipherNames, val) + return nil +} + +func (s *cipherSlice) Replace(val []string) error { + s.cipherNames = make([]string, 0, len(val)) + s.cipherNums = make([]uint16, 0, len(val)) + for _, cipher := range val { + if err := s.Append(cipher); err != nil { + return err + } + } + return nil +} + +func (s *cipherSlice) GetSlice() []string { + return s.cipherNames +} + +// Definition of the type for `--tls-custom-curves` +type curveSlice struct { + curveNums []tls.CurveID + curveNames []string +} + +func (s *curveSlice) Set(val string) error { + v, err := readAsCSV(val) + if err != nil { + return err + } + return s.Replace(v) +} + +func (s *curveSlice) Type() string { + return "stringSlice" +} + +func (s *curveSlice) String() string { + str, _ := writeAsCSV(s.curveNames) + return "[" + str + "]" +} + +func (s *curveSlice) Append(val string) error { + num := curveId(val) + if num == 0 { + return fmt.Errorf("unknown curve %q", val) + } + s.curveNums = append(s.curveNums, num) + s.curveNames = append(s.curveNames, val) + return nil +} + +func (s *curveSlice) Replace(val []string) error { + s.curveNames = make([]string, 0, len(val)) + s.curveNums = make([]tls.CurveID, 0, len(val)) + for _, curve := range val { + if err := s.Append(curve); err != nil { + return err + } + } + return nil +} + +func (s *curveSlice) GetSlice() []string { + return s.curveNames +} + +// Definition of the type for `--tls-custom-version` +type tlsVersion uint16 + +var tlsVersions = map[string]uint16{ + "TLSv1.0": tls.VersionTLS10, + "TLSv1.1": tls.VersionTLS11, + "TLSv1.2": tls.VersionTLS12, + "TLSv1.3": tls.VersionTLS13, +} + +func (v *tlsVersion) String() string { + for s, n := range tlsVersions { + if *v == tlsVersion(n) { + return s + } + } + return "" +} + +func (v *tlsVersion) Type() string { + return "string" +} + +func (v *tlsVersion) Set(value string) error { + n, ok := tlsVersions[value] + if !ok { + return fmt.Errorf("unknown TLS version: %q", value) + } + *v = tlsVersion(n) + return nil +} diff --git a/internal/shared/util/tlsprofiles/mozilla_data.go b/internal/shared/util/tlsprofiles/mozilla_data.go new file mode 100644 index 0000000000..6b74ade2d8 --- /dev/null +++ b/internal/shared/util/tlsprofiles/mozilla_data.go @@ -0,0 +1,87 @@ +package tlsprofiles + +// DO NOT EDIT, GENERATED BY hack/tools/update-tls-profiles.sh +// DATA SOURCE: https://ssl-config.mozilla.org/guidelines/latest.json +// DATA VERSION: 5.7 + +import ( + "crypto/tls" +) + +var modernTLSProfile = tlsProfile{ + ciphers: cipherSlice{ + cipherNums: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + }, + }, + curves: curveSlice{ + curveNums: []tls.CurveID{ + X25519, + prime256v1, + secp384r1, + }, + }, + minTLSVersion: tls.VersionTLS13, +} + +var intermediateTLSProfile = tlsProfile{ + ciphers: cipherSlice{ + cipherNums: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + }, + }, + curves: curveSlice{ + curveNums: []tls.CurveID{ + X25519, + prime256v1, + secp384r1, + }, + }, + minTLSVersion: tls.VersionTLS12, +} + +var oldTLSProfile = tlsProfile{ + ciphers: cipherSlice{ + cipherNums: []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + }, + }, + curves: curveSlice{ + curveNums: []tls.CurveID{ + X25519, + prime256v1, + secp384r1, + }, + }, + minTLSVersion: tls.VersionTLS10, +} diff --git a/internal/shared/util/tlsprofiles/tlsprofiles.go b/internal/shared/util/tlsprofiles/tlsprofiles.go new file mode 100644 index 0000000000..bac6586101 --- /dev/null +++ b/internal/shared/util/tlsprofiles/tlsprofiles.go @@ -0,0 +1,91 @@ +package tlsprofiles + +import ( + "crypto/tls" + "fmt" + "strings" +) + +var ( + configuredProfile tlsProfileName = "intermediate" + customTLSProfile tlsProfile = tlsProfile{ + ciphers: cipherSlice{}, + curves: curveSlice{}, + minTLSVersion: tls.VersionTLS12, + } +) + +type tlsProfile struct { + ciphers cipherSlice + curves curveSlice + minTLSVersion tlsVersion +} + +// Based on compatibility levels from: https://wiki.mozilla.org/Security/Server_Side_TLS +var profiles = map[string]*tlsProfile{ + "modern": &modernTLSProfile, + "intermediate": &intermediateTLSProfile, + "old": &oldTLSProfile, + "custom": &customTLSProfile, +} + +func findTLSProfile(profile tlsProfileName) (*tlsProfile, error) { + p := strings.ToLower(profile.String()) + tlsProfile, ok := profiles[p] + if !ok { + return nil, fmt.Errorf("unknown TLS profile: %q", profile.String()) + } + return tlsProfile, nil +} + +func GetTLSConfigFunc() (func(*tls.Config), error) { + tlsProfile, err := findTLSProfile(configuredProfile) + if err != nil { + return nil, err + } + return func(config *tls.Config) { + config.MinVersion = uint16(tlsProfile.minTLSVersion) + config.CipherSuites = tlsProfile.ciphers.cipherNums + config.CurvePreferences = tlsProfile.curves.curveNums + }, nil +} + +// This function should _really_ exist in crypto/tls +// cipherSuiteId returns the cipher suite ID given a standard name +// (e.g. "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"), or 0 if no such cipher exists +func cipherSuiteId(name string) uint16 { + for _, c := range tls.CipherSuites() { + if c.Name == name { + return c.ID + } + } + for _, c := range tls.InsecureCipherSuites() { + if c.Name == name { + return c.ID + } + } + return 0 +} + +// This is primarily so that we don't have to rewrite curve values in mozilla_data.go +const ( + X25519 tls.CurveID = tls.X25519 + prime256v1 tls.CurveID = tls.CurveP256 + secp384r1 tls.CurveID = tls.CurveP384 + secp521r1 tls.CurveID = tls.CurveP521 +) + +var curves = map[string]tls.CurveID{ + "X25519": tls.X25519, + "prime256v1": tls.CurveP256, + "secp384r1": tls.CurveP384, + "secp521r1": tls.CurveP521, +} + +// Returns 0 for an invalid curve name +func curveId(name string) tls.CurveID { + if id, ok := curves[name]; ok { + return id + } + return 0 +} diff --git a/internal/shared/util/tlsprofiles/tlsprofiles_test.go b/internal/shared/util/tlsprofiles/tlsprofiles_test.go new file mode 100644 index 0000000000..7be0903f79 --- /dev/null +++ b/internal/shared/util/tlsprofiles/tlsprofiles_test.go @@ -0,0 +1,160 @@ +package tlsprofiles + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetProfiles(t *testing.T) { + tests := []struct { + name tlsProfileName + result bool + }{ + {"modern", true}, + {"intermediate", true}, + {"old", true}, + {"custom", true}, + {"does-not-exist", false}, + } + + for _, test := range tests { + p, err := findTLSProfile(test.name) + if !test.result { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, p) + } + } +} + +func TestGetTLSConfigFunc(t *testing.T) { + f, err := GetTLSConfigFunc() + require.NoError(t, err) + require.NotNil(t, f) + + // Set an invalid profile + configuredProfile = "does-not-exist" + f, err = GetTLSConfigFunc() + require.Error(t, err) + require.Nil(t, f) +} + +func TestCipherStuiteId(t *testing.T) { + tests := []struct { + name string + result uint16 + }{ + {"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 0xC009}, + {"unknown-cipher", 0}, + {"TLS_RSA_WITH_3DES_EDE_CBC_SHA", 0x000A}, // Insecure cipher + {"DHE-RSA-AES128-SHA256", 0}, // Valid OpenSSL cipher, not implemented + } + + for _, test := range tests { + v := cipherSuiteId(test.name) + require.Equal(t, test.result, v) + } +} + +func TestSetProfileName(t *testing.T) { + var profile tlsProfileName + + tests := []struct { + name string + result bool + }{ + {"modern", true}, + {"intermediate", true}, + {"old", true}, + {"custom", true}, + {"does-not-exist", false}, + } + + for _, test := range tests { + err := (&profile).Set(test.name) + if !test.result { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} + +func TestSetCustomCipher(t *testing.T) { + var ciphers cipherSlice + + tests := []struct { + name string + result bool + }{ + {"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", true}, + {"unknown-cipher", false}, + {"TLS_RSA_WITH_3DES_EDE_CBC_SHA", true}, // Insecure cipher + {"DHE-RSA-AES128-SHA256", false}, // Valid OpenSSL cipher, not implemented + {"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_3DES_EDE_CBC_SHA", true}, // Multiple + } + + for _, test := range tests { + err := ciphers.Set(test.name) + if test.result { + require.NoError(t, err) + require.Equal(t, "["+test.name+"]", ciphers.String()) + } else { + require.Error(t, err) + } + } +} + +func TestSetCustomCurves(t *testing.T) { + var curves curveSlice + + tests := []struct { + name string + result bool + }{ + {"X25519", true}, + {"prime256v1", true}, + {"secp384r1", true}, + {"secp521r1", true}, + {"unknown-cuve", false}, + {"X448", false}, // Valid OpenSSL curve, not implemented + {"X25519,prime256v1", true}, // Multiple + } + + for _, test := range tests { + err := curves.Set(test.name) + if test.result { + require.NoError(t, err) + require.Equal(t, "["+test.name+"]", curves.String()) + } else { + require.Error(t, err) + } + } +} + +func TestSetCustomVersion(t *testing.T) { + var version tlsVersion + + tests := []struct { + name string + result bool + }{ + {"TLSv1.0", true}, + {"TLSv1.1", true}, + {"TLSv1.2", true}, + {"TLSv1.3", true}, + {"unknown-version", false}, + } + + for _, test := range tests { + err := version.Set(test.name) + if test.result { + require.NoError(t, err) + require.Equal(t, test.name, version.String()) + } else { + require.Error(t, err) + } + } +} diff --git a/kind-config.yaml b/kind-config.yaml index 6cdeef6220..5b5b3b9139 100644 --- a/kind-config.yaml +++ b/kind-config.yaml @@ -8,9 +8,21 @@ nodes: hostPort: 30000 listenAddress: "127.0.0.1" protocol: tcp + # prometheus metrics service's NodePort + - containerPort: 30900 + hostPort: 30900 + listenAddress: "127.0.0.1" + protocol: tcp kubeadmConfigPatches: - | kind: ClusterConfiguration apiServer: extraArgs: enable-admission-plugins: OwnerReferencesPermissionEnforcement + extraMounts: + - hostPath: ./hack/kind-config/containerd/certs.d + containerPath: /etc/containerd/certs.d +containerdConfigPatches: + - |- + [plugins."io.containerd.grpc.v1.cri".registry] + config_path = "/etc/containerd/certs.d" diff --git a/manifests/OWNERS b/manifests/OWNERS new file mode 100644 index 0000000000..b44dad0ea8 --- /dev/null +++ b/manifests/OWNERS @@ -0,0 +1,2 @@ +approvers: + - manifest-approvers diff --git a/config/catalogs/clustercatalogs/default-catalogs.yaml b/manifests/default-catalogs.yaml similarity index 100% rename from config/catalogs/clustercatalogs/default-catalogs.yaml rename to manifests/default-catalogs.yaml diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml new file mode 100644 index 0000000000..5b09dc949c --- /dev/null +++ b/manifests/experimental-e2e.yaml @@ -0,0 +1,2453 @@ +--- +# Source: olmv1/templates/namespace.yml +apiVersion: v1 +kind: Namespace +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + app.kubernetes.io/part-of: olm + name: olmv1-system +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 7443 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 9443 + protocol: TCP + podSelector: + matchLabels: + control-plane: catalogd-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: default-deny-all-traffic + namespace: olmv1-system +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + control-plane: operator-controller-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml +apiVersion: v1 +data: + registries.conf: | + [[registry]] + prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" + location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" +kind: ConfigMap +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-registries-conf + namespace: olmv1-system +--- +# Source: olmv1/templates/e2e/persistentvolumeclaim-olmv1-system-e2e-coverage.yml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-coverage + namespace: olmv1-system +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64Mi +--- +# Source: olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clustercatalogs.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterCatalog + listKind: ClusterCatalogList + plural: clustercatalogs + singular: clustercatalog + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.lastUnpacked + name: LastUnpacked + type: date + - jsonPath: .status.conditions[?(@.type=="Serving")].status + name: Serving + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec is the desired state of the ClusterCatalog. + spec is required. + The controller will work to ensure that the desired + catalog is unpacked and served over the catalog content HTTP server. + properties: + availabilityMode: + default: Available + description: |- + availabilityMode allows users to define how the ClusterCatalog is made available to clients on the cluster. + availabilityMode is optional. + + Allowed values are "Available" and "Unavailable" and omitted. + + When omitted, the default value is "Available". + + When set to "Available", the catalog contents will be unpacked and served over the catalog content HTTP server. + Setting the availabilityMode to "Available" tells clients that they should consider this ClusterCatalog + and its contents as usable. + + When set to "Unavailable", the catalog contents will no longer be served over the catalog content HTTP server. + When set to this availabilityMode it should be interpreted the same as the ClusterCatalog not existing. + Setting the availabilityMode to "Unavailable" can be useful in scenarios where a user may not want + to delete the ClusterCatalog all together, but would still like it to be treated as if it doesn't exist. + enum: + - Unavailable + - Available + type: string + priority: + default: 0 + description: |- + priority allows the user to define a priority for a ClusterCatalog. + priority is optional. + + A ClusterCatalog's priority is used by clients as a tie-breaker between ClusterCatalogs that meet the client's requirements. + A higher number means higher priority. + + It is up to clients to decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + When deciding how to break the tie in this scenario, it is recommended that clients prompt their users for additional input. + + When omitted, the default priority is 0 because that is the zero value of integers. + + Negative numbers can be used to specify a priority lower than the default. + Positive numbers can be used to specify a priority higher than the default. + + The lowest possible value is -2147483648. + The highest possible value is 2147483647. + format: int32 + type: integer + source: + description: |- + source allows a user to define the source of a catalog. + A "catalog" contains information on content that can be installed on a cluster. + Providing a catalog source makes the contents of the catalog discoverable and usable by + other on-cluster components. + These on-cluster components may do a variety of things with this information, such as + presenting the content in a GUI dashboard or installing content from the catalog on the cluster. + The catalog source must contain catalog metadata in the File-Based Catalog (FBC) format. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs. + source is a required field. + + Below is a minimal example of a ClusterCatalogSpec that sources a catalog from an image: + + source: + type: Image + image: + ref: quay.io/operatorhubio/catalog:latest + properties: + image: + description: |- + image is used to configure how catalog contents are sourced from an OCI image. + This field is required when type is Image, and forbidden otherwise. + properties: + pollIntervalMinutes: + description: |- + pollIntervalMinutes allows the user to set the interval, in minutes, at which the image source should be polled for new content. + pollIntervalMinutes is optional. + pollIntervalMinutes can not be specified when ref is a digest-based reference. + + When omitted, the image will not be polled for new content. + minimum: 1 + type: integer + ref: + description: |- + ref allows users to define the reference to a container image containing Catalog contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/catalog", "catalog", "my-catalog.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/catalog". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + x-kubernetes-validations: + - message: cannot specify pollIntervalMinutes while using digest-based + image + rule: 'self.ref.find(''(@.*:)'') != "" ? !has(self.pollIntervalMinutes) + : true' + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When using an image source, the image field must be set and must be the only field defined for this type. + enum: + - Image + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + required: + - source + type: object + status: + description: |- + status contains information about the state of the ClusterCatalog such as: + - Whether or not the catalog contents are being served via the catalog content HTTP server + - Whether or not the ClusterCatalog is progressing to a new state + - A reference to the source from which the catalog contents were retrieved + properties: + conditions: + description: |- + conditions is a representation of the current state for this ClusterCatalog. + + The current condition types are Serving and Progressing. + + The Serving condition is used to represent whether or not the contents of the catalog is being served via the HTTP(S) web server. + When it has a status of True and a reason of Available, the contents of the catalog are being served. + When it has a status of False and a reason of Unavailable, the contents of the catalog are not being served because the contents are not yet available. + When it has a status of False and a reason of UserSpecifiedUnavailable, the contents of the catalog are not being served because the catalog has been intentionally marked as unavailable. + + The Progressing condition is used to represent whether or not the ClusterCatalog is progressing or is ready to progress towards a new state. + When it has a status of True and a reason of Retrying, there was an error in the progression of the ClusterCatalog that may be resolved on subsequent reconciliation attempts. + When it has a status of True and a reason of Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + When it has a status of False and a reason of Blocked, there was an error in the progression of the ClusterCatalog that requires manual intervention for recovery. + + In the case that the Serving condition is True with reason Available and Progressing is True with reason Retrying, the previously fetched + catalog contents are still being served via the HTTP(S) web server while we are progressing towards serving a new version of the catalog + contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes + to the contents we identify that there are updates to the contents. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUnpacked: + description: |- + lastUnpacked represents the last time the contents of the + catalog were extracted from their source format. As an example, + when using an Image source, the OCI image will be pulled and the + image layers written to a file-system backed cache. We refer to the + act of this extraction from the source format as "unpacking". + format: date-time + type: string + resolvedSource: + description: resolvedSource contains information about the resolved + source based on the source type. + properties: + image: + description: |- + image is a field containing resolution information for a catalog sourced from an image. + This field must be set when type is Image, and forbidden otherwise. + properties: + ref: + description: |- + ref contains the resolved image digest-based reference. + The digest format is used so users can use other tooling to fetch the exact + OCI manifests that were used to extract the catalog contents. + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest + rule: self.find('(@.*:)') != "" + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", information about the resolved image source will be set in the 'image' field. + enum: + - Image + type: string + required: + - image + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + urls: + description: urls contains the URLs that can be used to access the + catalog. + properties: + base: + description: |- + base is a cluster-internal URL that provides endpoints for + accessing the content of the catalog. + + It is expected that clients append the path for the endpoint they wish + to access. + + Currently, only a single endpoint is served and is accessible at the path + /api/v1. + + The endpoints served for the v1 API are: + - /all - this endpoint returns the entirety of the catalog contents in the FBC format + + As the needs of users and clients of the evolve, new endpoints may be added. + maxLength: 525 + type: string + x-kubernetes-validations: + - message: must be a valid URL + rule: isURL(self) + - message: scheme must be either http or https + rule: 'isURL(self) ? (url(https://github.com/self).getScheme() == "http" || url(https://github.com/self).getScheme() + == "https") : true' + required: + - base + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensionrevisions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + description: |- + Phases are groups of objects that will be applied at the same time. + All objects in the phase will have to pass their probes in order to progress to the next phase. + items: + description: |- + ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. + All objects in the a phase will have to pass their probes in order to progress to the next phase. + properties: + name: + description: Name identifies this phase. + maxLength: 63 + pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ + type: string + objects: + description: Objects are a list of all the objects within this + phase. + items: + description: ClusterExtensionRevisionObject contains an object + and settings for it. + properties: + collisionProtection: + default: Prevent + description: |- + CollisionProtection controls whether OLM can adopt and modify objects + already existing on the cluster or even owned by another controller. + type: string + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + description: Previous references previous revisions that objects can + be adopted from. + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + description: |- + Revision is a sequence number representing a specific revision of the ClusterExtension instance. + Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + format: int64 + minimum: 1 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: catalogd-manager-role + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental-e2e +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/status + verbs: + - get + - patch + - update +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-clusterextension-viewer-role +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-controller-manager-role + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental-e2e +rules: + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-proxy-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-proxy-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-admin-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: catalogd-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental-e2e +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: operator-controller-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental-e2e +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-leader-election-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-leader-election-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/service-olmv1-system-catalogd-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service + namespace: olmv1-system +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + - name: webhook + port: 9443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 7443 + protocol: TCP + targetPort: 7443 + selector: + app.kubernetes.io/name: catalogd +--- +# Source: olmv1/templates/service-olmv1-system-operator-controller-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-service + namespace: olmv1-system +spec: + ports: + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: operator-controller +--- +# Source: olmv1/templates/e2e/pod-olmv1-system-e2e-coverage-copy-pod.yml +apiVersion: v1 +kind: Pod +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-coverage-copy-pod + namespace: olmv1-system +spec: + containers: + - command: + - sleep + - infinity + image: busybox:1.36 + name: tar + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /e2e-coverage + name: e2e-coverage-volume + readOnly: true + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + volumes: + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + readOnly: true +--- +# Source: olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + minReadySeconds: 5 + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: catalogd-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + control-plane: catalogd-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --leader-elect + - --metrics-bind-address=:7443 + - --external-address=catalogd-service.olmv1-system.svc + - --feature-gates=APIV1MetasHandler=true + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --pull-cas-dir=/var/ca-certs + - --tls-profile=custom + - --tls-custom-version=TLSv1.3 + - --tls-custom-curves=X25519,prime256v1 + - --tls-custom-ciphers=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384 + command: + - ./catalogd + env: + - name: GOCOVERDIR + value: /e2e-coverage + image: "quay.io/operator-framework/catalogd:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - mountPath: /e2e-coverage + name: e2e-coverage-volume + - mountPath: /var/cache/ + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: catalogd-controller-manager + volumes: + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogd-service-cert-git-version + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: catalogd-service-cert-git-version + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: operator-controller-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: operator-controller + control-plane: operator-controller-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8443 + - --leader-elect + - --feature-gates=PreflightPermissions=true + - --feature-gates=HelmChartSupport=true + - --feature-gates=BoxcutterRuntime=true + - --feature-gates=WebhookProviderOpenshiftServiceCA=false + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --pull-cas-dir=/var/ca-certs + - --tls-profile=modern + command: + - /operator-controller + env: + - name: GOCOVERDIR + value: /e2e-coverage + image: "quay.io/operator-framework/operator-controller:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /etc/containers + name: e2e-registries-conf + - mountPath: /e2e-coverage + name: e2e-coverage-volume + - mountPath: /var/cache + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: operator-controller-certs + readOnly: true + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: operator-controller-controller-manager + volumes: + - configMap: + name: e2e-registries-conf + name: e2e-registries-conf + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: operator-controller-cert + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca + namespace: cert-manager +spec: + commonName: olmv1-ca + isCA: true + issuerRef: + group: cert-manager.io + kind: Issuer + name: self-sign-issuer + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: olmv1-ca + secretTemplate: + annotations: + cert-manager.io/allow-direct-injection: "true" +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service-cert + namespace: olmv1-system +spec: + dnsNames: + - localhost + - catalogd-service.olmv1-system.svc + - catalogd-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: catalogd-service-cert-git-version +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: operator-controller-cert + namespace: olmv1-system +spec: + dnsNames: + - operator-controller-service.olmv1-system.svc + - operator-controller-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: operator-controller-cert +--- +# Source: olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca +spec: + ca: + secretName: olmv1-ca +--- +# Source: olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: self-sign-issuer + namespace: cert-manager +spec: + selfSigned: {} +--- +# Source: olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: catalogd-mutating-webhook-configuration + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + cert-manager.io/inject-ca-from-secret: cert-manager/olmv1-ca + olm.operatorframework.io/feature-set: experimental-e2e +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: catalogd-service + namespace: olmv1-system + path: /mutate-olm-operatorframework-io-v1-clustercatalog + port: 9443 + failurePolicy: Fail + name: inject-metadata-name.olm.operatorframework.io + rules: + - apiGroups: + - olm.operatorframework.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clustercatalogs + sideEffects: None + timeoutSeconds: 10 + matchConditions: + - name: MissingOrIncorrectMetadataNameLabel + expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml new file mode 100644 index 0000000000..4ddd450779 --- /dev/null +++ b/manifests/experimental.yaml @@ -0,0 +1,2352 @@ +--- +# Source: olmv1/templates/namespace.yml +apiVersion: v1 +kind: Namespace +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + app.kubernetes.io/part-of: olm + name: olmv1-system +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 7443 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 9443 + protocol: TCP + podSelector: + matchLabels: + control-plane: catalogd-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: default-deny-all-traffic + namespace: olmv1-system +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + control-plane: operator-controller-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clustercatalogs.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterCatalog + listKind: ClusterCatalogList + plural: clustercatalogs + singular: clustercatalog + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.lastUnpacked + name: LastUnpacked + type: date + - jsonPath: .status.conditions[?(@.type=="Serving")].status + name: Serving + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec is the desired state of the ClusterCatalog. + spec is required. + The controller will work to ensure that the desired + catalog is unpacked and served over the catalog content HTTP server. + properties: + availabilityMode: + default: Available + description: |- + availabilityMode allows users to define how the ClusterCatalog is made available to clients on the cluster. + availabilityMode is optional. + + Allowed values are "Available" and "Unavailable" and omitted. + + When omitted, the default value is "Available". + + When set to "Available", the catalog contents will be unpacked and served over the catalog content HTTP server. + Setting the availabilityMode to "Available" tells clients that they should consider this ClusterCatalog + and its contents as usable. + + When set to "Unavailable", the catalog contents will no longer be served over the catalog content HTTP server. + When set to this availabilityMode it should be interpreted the same as the ClusterCatalog not existing. + Setting the availabilityMode to "Unavailable" can be useful in scenarios where a user may not want + to delete the ClusterCatalog all together, but would still like it to be treated as if it doesn't exist. + enum: + - Unavailable + - Available + type: string + priority: + default: 0 + description: |- + priority allows the user to define a priority for a ClusterCatalog. + priority is optional. + + A ClusterCatalog's priority is used by clients as a tie-breaker between ClusterCatalogs that meet the client's requirements. + A higher number means higher priority. + + It is up to clients to decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + When deciding how to break the tie in this scenario, it is recommended that clients prompt their users for additional input. + + When omitted, the default priority is 0 because that is the zero value of integers. + + Negative numbers can be used to specify a priority lower than the default. + Positive numbers can be used to specify a priority higher than the default. + + The lowest possible value is -2147483648. + The highest possible value is 2147483647. + format: int32 + type: integer + source: + description: |- + source allows a user to define the source of a catalog. + A "catalog" contains information on content that can be installed on a cluster. + Providing a catalog source makes the contents of the catalog discoverable and usable by + other on-cluster components. + These on-cluster components may do a variety of things with this information, such as + presenting the content in a GUI dashboard or installing content from the catalog on the cluster. + The catalog source must contain catalog metadata in the File-Based Catalog (FBC) format. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs. + source is a required field. + + Below is a minimal example of a ClusterCatalogSpec that sources a catalog from an image: + + source: + type: Image + image: + ref: quay.io/operatorhubio/catalog:latest + properties: + image: + description: |- + image is used to configure how catalog contents are sourced from an OCI image. + This field is required when type is Image, and forbidden otherwise. + properties: + pollIntervalMinutes: + description: |- + pollIntervalMinutes allows the user to set the interval, in minutes, at which the image source should be polled for new content. + pollIntervalMinutes is optional. + pollIntervalMinutes can not be specified when ref is a digest-based reference. + + When omitted, the image will not be polled for new content. + minimum: 1 + type: integer + ref: + description: |- + ref allows users to define the reference to a container image containing Catalog contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/catalog", "catalog", "my-catalog.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/catalog". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + x-kubernetes-validations: + - message: cannot specify pollIntervalMinutes while using digest-based + image + rule: 'self.ref.find(''(@.*:)'') != "" ? !has(self.pollIntervalMinutes) + : true' + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When using an image source, the image field must be set and must be the only field defined for this type. + enum: + - Image + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + required: + - source + type: object + status: + description: |- + status contains information about the state of the ClusterCatalog such as: + - Whether or not the catalog contents are being served via the catalog content HTTP server + - Whether or not the ClusterCatalog is progressing to a new state + - A reference to the source from which the catalog contents were retrieved + properties: + conditions: + description: |- + conditions is a representation of the current state for this ClusterCatalog. + + The current condition types are Serving and Progressing. + + The Serving condition is used to represent whether or not the contents of the catalog is being served via the HTTP(S) web server. + When it has a status of True and a reason of Available, the contents of the catalog are being served. + When it has a status of False and a reason of Unavailable, the contents of the catalog are not being served because the contents are not yet available. + When it has a status of False and a reason of UserSpecifiedUnavailable, the contents of the catalog are not being served because the catalog has been intentionally marked as unavailable. + + The Progressing condition is used to represent whether or not the ClusterCatalog is progressing or is ready to progress towards a new state. + When it has a status of True and a reason of Retrying, there was an error in the progression of the ClusterCatalog that may be resolved on subsequent reconciliation attempts. + When it has a status of True and a reason of Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + When it has a status of False and a reason of Blocked, there was an error in the progression of the ClusterCatalog that requires manual intervention for recovery. + + In the case that the Serving condition is True with reason Available and Progressing is True with reason Retrying, the previously fetched + catalog contents are still being served via the HTTP(S) web server while we are progressing towards serving a new version of the catalog + contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes + to the contents we identify that there are updates to the contents. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUnpacked: + description: |- + lastUnpacked represents the last time the contents of the + catalog were extracted from their source format. As an example, + when using an Image source, the OCI image will be pulled and the + image layers written to a file-system backed cache. We refer to the + act of this extraction from the source format as "unpacking". + format: date-time + type: string + resolvedSource: + description: resolvedSource contains information about the resolved + source based on the source type. + properties: + image: + description: |- + image is a field containing resolution information for a catalog sourced from an image. + This field must be set when type is Image, and forbidden otherwise. + properties: + ref: + description: |- + ref contains the resolved image digest-based reference. + The digest format is used so users can use other tooling to fetch the exact + OCI manifests that were used to extract the catalog contents. + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest + rule: self.find('(@.*:)') != "" + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", information about the resolved image source will be set in the 'image' field. + enum: + - Image + type: string + required: + - image + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + urls: + description: urls contains the URLs that can be used to access the + catalog. + properties: + base: + description: |- + base is a cluster-internal URL that provides endpoints for + accessing the content of the catalog. + + It is expected that clients append the path for the endpoint they wish + to access. + + Currently, only a single endpoint is served and is accessible at the path + /api/v1. + + The endpoints served for the v1 API are: + - /all - this endpoint returns the entirety of the catalog contents in the FBC format + + As the needs of users and clients of the evolve, new endpoints may be added. + maxLength: 525 + type: string + x-kubernetes-validations: + - message: must be a valid URL + rule: isURL(self) + - message: scheme must be either http or https + rule: 'isURL(self) ? (url(https://github.com/self).getScheme() == "http" || url(https://github.com/self).getScheme() + == "https") : true' + required: + - base + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensionrevisions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensionrevisions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtensionRevision + listKind: ClusterExtensionRevisionList + plural: clusterextensionrevisions + singular: clusterextensionrevision + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtensionRevision is the Schema for the clusterextensionrevisions + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + lifecycleState: + default: Active + description: Specifies the lifecycle state of the ClusterExtensionRevision. + enum: + - Active + - Paused + - Archived + type: string + x-kubernetes-validations: + - message: can not un-archive + rule: oldSelf == 'Active' || oldSelf == 'Paused' || oldSelf == 'Archived' + && oldSelf == self + phases: + description: |- + Phases are groups of objects that will be applied at the same time. + All objects in the phase will have to pass their probes in order to progress to the next phase. + items: + description: |- + ClusterExtensionRevisionPhase are groups of objects that will be applied at the same time. + All objects in the a phase will have to pass their probes in order to progress to the next phase. + properties: + name: + description: Name identifies this phase. + maxLength: 63 + pattern: ^[a-z]([-a-z0-9]*[a-z0-9])?$ + type: string + objects: + description: Objects are a list of all the objects within this + phase. + items: + description: ClusterExtensionRevisionObject contains an object + and settings for it. + properties: + collisionProtection: + default: Prevent + description: |- + CollisionProtection controls whether OLM can adopt and modify objects + already existing on the cluster or even owned by another controller. + type: string + object: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + required: + - object + type: object + type: array + required: + - name + - objects + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: phases is immutable + rule: self == oldSelf || oldSelf.size() == 0 + previous: + description: Previous references previous revisions that objects can + be adopted from. + items: + properties: + name: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - uid + type: object + type: array + x-kubernetes-validations: + - message: previous is immutable + rule: self == oldSelf + revision: + description: |- + Revision is a sequence number representing a specific revision of the ClusterExtension instance. + Must be positive. Each ClusterExtensionRevision of the same parent ClusterExtension needs to have + a unique value assigned. It is immutable after creation. The new revision number must always be previous revision +1. + format: int64 + minimum: 1 + type: integer + x-kubernetes-validations: + - message: revision is immutable + rule: self == oldSelf + required: + - revision + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: experimental + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: catalogd-manager-role + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/status + verbs: + - get + - patch + - update +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-clusterextension-viewer-role +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-controller-manager-role + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental +rules: + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-proxy-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-proxy-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-admin-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: catalogd-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: operator-controller-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: experimental +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-leader-election-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-leader-election-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/service-olmv1-system-catalogd-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service + namespace: olmv1-system +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + - name: webhook + port: 9443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 7443 + protocol: TCP + targetPort: 7443 + selector: + app.kubernetes.io/name: catalogd +--- +# Source: olmv1/templates/service-olmv1-system-operator-controller-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-service + namespace: olmv1-system +spec: + ports: + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: operator-controller +--- +# Source: olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + minReadySeconds: 5 + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: catalogd-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + control-plane: catalogd-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --leader-elect + - --metrics-bind-address=:7443 + - --external-address=catalogd-service.olmv1-system.svc + - --feature-gates=APIV1MetasHandler=true + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --pull-cas-dir=/var/ca-certs + command: + - ./catalogd + image: "quay.io/operator-framework/catalogd:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - mountPath: /var/cache/ + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: catalogd-controller-manager + volumes: + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogd-service-cert-git-version + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: catalogd-service-cert-git-version + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: operator-controller-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: operator-controller + control-plane: operator-controller-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8443 + - --leader-elect + - --feature-gates=PreflightPermissions=true + - --feature-gates=HelmChartSupport=true + - --feature-gates=BoxcutterRuntime=true + - --feature-gates=WebhookProviderOpenshiftServiceCA=false + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --pull-cas-dir=/var/ca-certs + command: + - /operator-controller + image: "quay.io/operator-framework/operator-controller:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /var/cache + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: operator-controller-certs + readOnly: true + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: operator-controller-controller-manager + volumes: + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: operator-controller-cert + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca + namespace: cert-manager +spec: + commonName: olmv1-ca + isCA: true + issuerRef: + group: cert-manager.io + kind: Issuer + name: self-sign-issuer + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: olmv1-ca + secretTemplate: + annotations: + cert-manager.io/allow-direct-injection: "true" +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service-cert + namespace: olmv1-system +spec: + dnsNames: + - localhost + - catalogd-service.olmv1-system.svc + - catalogd-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: catalogd-service-cert-git-version +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: operator-controller-cert + namespace: olmv1-system +spec: + dnsNames: + - operator-controller-service.olmv1-system.svc + - operator-controller-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: operator-controller-cert +--- +# Source: olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca +spec: + ca: + secretName: olmv1-ca +--- +# Source: olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + annotations: + olm.operatorframework.io/feature-set: experimental + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: self-sign-issuer + namespace: cert-manager +spec: + selfSigned: {} +--- +# Source: olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: catalogd-mutating-webhook-configuration + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + cert-manager.io/inject-ca-from-secret: cert-manager/olmv1-ca + olm.operatorframework.io/feature-set: experimental +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: catalogd-service + namespace: olmv1-system + path: /mutate-olm-operatorframework-io-v1-clustercatalog + port: 9443 + failurePolicy: Fail + name: inject-metadata-name.olm.operatorframework.io + rules: + - apiGroups: + - olm.operatorframework.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clustercatalogs + sideEffects: None + timeoutSeconds: 10 + matchConditions: + - name: MissingOrIncorrectMetadataNameLabel + expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml new file mode 100644 index 0000000000..9c7a20f573 --- /dev/null +++ b/manifests/standard-e2e.yaml @@ -0,0 +1,2234 @@ +--- +# Source: olmv1/templates/namespace.yml +apiVersion: v1 +kind: Namespace +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + app.kubernetes.io/part-of: olm + name: olmv1-system +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 7443 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 9443 + protocol: TCP + podSelector: + matchLabels: + control-plane: catalogd-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: default-deny-all-traffic + namespace: olmv1-system +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + control-plane: operator-controller-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml +apiVersion: v1 +data: + registries.conf: | + [[registry]] + prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" + location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" +kind: ConfigMap +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-registries-conf + namespace: olmv1-system +--- +# Source: olmv1/templates/e2e/persistentvolumeclaim-olmv1-system-e2e-coverage.yml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-coverage + namespace: olmv1-system +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64Mi +--- +# Source: olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard + name: clustercatalogs.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterCatalog + listKind: ClusterCatalogList + plural: clustercatalogs + singular: clustercatalog + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.lastUnpacked + name: LastUnpacked + type: date + - jsonPath: .status.conditions[?(@.type=="Serving")].status + name: Serving + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec is the desired state of the ClusterCatalog. + spec is required. + The controller will work to ensure that the desired + catalog is unpacked and served over the catalog content HTTP server. + properties: + availabilityMode: + default: Available + description: |- + availabilityMode allows users to define how the ClusterCatalog is made available to clients on the cluster. + availabilityMode is optional. + + Allowed values are "Available" and "Unavailable" and omitted. + + When omitted, the default value is "Available". + + When set to "Available", the catalog contents will be unpacked and served over the catalog content HTTP server. + Setting the availabilityMode to "Available" tells clients that they should consider this ClusterCatalog + and its contents as usable. + + When set to "Unavailable", the catalog contents will no longer be served over the catalog content HTTP server. + When set to this availabilityMode it should be interpreted the same as the ClusterCatalog not existing. + Setting the availabilityMode to "Unavailable" can be useful in scenarios where a user may not want + to delete the ClusterCatalog all together, but would still like it to be treated as if it doesn't exist. + enum: + - Unavailable + - Available + type: string + priority: + default: 0 + description: |- + priority allows the user to define a priority for a ClusterCatalog. + priority is optional. + + A ClusterCatalog's priority is used by clients as a tie-breaker between ClusterCatalogs that meet the client's requirements. + A higher number means higher priority. + + It is up to clients to decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + When deciding how to break the tie in this scenario, it is recommended that clients prompt their users for additional input. + + When omitted, the default priority is 0 because that is the zero value of integers. + + Negative numbers can be used to specify a priority lower than the default. + Positive numbers can be used to specify a priority higher than the default. + + The lowest possible value is -2147483648. + The highest possible value is 2147483647. + format: int32 + type: integer + source: + description: |- + source allows a user to define the source of a catalog. + A "catalog" contains information on content that can be installed on a cluster. + Providing a catalog source makes the contents of the catalog discoverable and usable by + other on-cluster components. + These on-cluster components may do a variety of things with this information, such as + presenting the content in a GUI dashboard or installing content from the catalog on the cluster. + The catalog source must contain catalog metadata in the File-Based Catalog (FBC) format. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs. + source is a required field. + + Below is a minimal example of a ClusterCatalogSpec that sources a catalog from an image: + + source: + type: Image + image: + ref: quay.io/operatorhubio/catalog:latest + properties: + image: + description: |- + image is used to configure how catalog contents are sourced from an OCI image. + This field is required when type is Image, and forbidden otherwise. + properties: + pollIntervalMinutes: + description: |- + pollIntervalMinutes allows the user to set the interval, in minutes, at which the image source should be polled for new content. + pollIntervalMinutes is optional. + pollIntervalMinutes can not be specified when ref is a digest-based reference. + + When omitted, the image will not be polled for new content. + minimum: 1 + type: integer + ref: + description: |- + ref allows users to define the reference to a container image containing Catalog contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/catalog", "catalog", "my-catalog.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/catalog". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + x-kubernetes-validations: + - message: cannot specify pollIntervalMinutes while using digest-based + image + rule: 'self.ref.find(''(@.*:)'') != "" ? !has(self.pollIntervalMinutes) + : true' + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When using an image source, the image field must be set and must be the only field defined for this type. + enum: + - Image + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + required: + - source + type: object + status: + description: |- + status contains information about the state of the ClusterCatalog such as: + - Whether or not the catalog contents are being served via the catalog content HTTP server + - Whether or not the ClusterCatalog is progressing to a new state + - A reference to the source from which the catalog contents were retrieved + properties: + conditions: + description: |- + conditions is a representation of the current state for this ClusterCatalog. + + The current condition types are Serving and Progressing. + + The Serving condition is used to represent whether or not the contents of the catalog is being served via the HTTP(S) web server. + When it has a status of True and a reason of Available, the contents of the catalog are being served. + When it has a status of False and a reason of Unavailable, the contents of the catalog are not being served because the contents are not yet available. + When it has a status of False and a reason of UserSpecifiedUnavailable, the contents of the catalog are not being served because the catalog has been intentionally marked as unavailable. + + The Progressing condition is used to represent whether or not the ClusterCatalog is progressing or is ready to progress towards a new state. + When it has a status of True and a reason of Retrying, there was an error in the progression of the ClusterCatalog that may be resolved on subsequent reconciliation attempts. + When it has a status of True and a reason of Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + When it has a status of False and a reason of Blocked, there was an error in the progression of the ClusterCatalog that requires manual intervention for recovery. + + In the case that the Serving condition is True with reason Available and Progressing is True with reason Retrying, the previously fetched + catalog contents are still being served via the HTTP(S) web server while we are progressing towards serving a new version of the catalog + contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes + to the contents we identify that there are updates to the contents. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUnpacked: + description: |- + lastUnpacked represents the last time the contents of the + catalog were extracted from their source format. As an example, + when using an Image source, the OCI image will be pulled and the + image layers written to a file-system backed cache. We refer to the + act of this extraction from the source format as "unpacking". + format: date-time + type: string + resolvedSource: + description: resolvedSource contains information about the resolved + source based on the source type. + properties: + image: + description: |- + image is a field containing resolution information for a catalog sourced from an image. + This field must be set when type is Image, and forbidden otherwise. + properties: + ref: + description: |- + ref contains the resolved image digest-based reference. + The digest format is used so users can use other tooling to fetch the exact + OCI manifests that were used to extract the catalog contents. + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest + rule: self.find('(@.*:)') != "" + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", information about the resolved image source will be set in the 'image' field. + enum: + - Image + type: string + required: + - image + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + urls: + description: urls contains the URLs that can be used to access the + catalog. + properties: + base: + description: |- + base is a cluster-internal URL that provides endpoints for + accessing the content of the catalog. + + It is expected that clients append the path for the endpoint they wish + to access. + + Currently, only a single endpoint is served and is accessible at the path + /api/v1. + + The endpoints served for the v1 API are: + - /all - this endpoint returns the entirety of the catalog contents in the FBC format + + As the needs of users and clients of the evolve, new endpoints may be added. + maxLength: 525 + type: string + x-kubernetes-validations: + - message: must be a valid URL + rule: isURL(self) + - message: scheme must be either http or https + rule: 'isURL(self) ? (url(https://github.com/self).getScheme() == "http" || url(https://github.com/self).getScheme() + == "https") : true' + required: + - base + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: catalogd-manager-role + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard-e2e +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/status + verbs: + - get + - patch + - update +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-clusterextension-viewer-role +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-controller-manager-role + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard-e2e +rules: + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-proxy-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-proxy-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: catalogd-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard-e2e +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: operator-controller-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard-e2e +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-leader-election-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-leader-election-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/service-olmv1-system-catalogd-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service + namespace: olmv1-system +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + - name: webhook + port: 9443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 7443 + protocol: TCP + targetPort: 7443 + selector: + app.kubernetes.io/name: catalogd +--- +# Source: olmv1/templates/service-olmv1-system-operator-controller-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-service + namespace: olmv1-system +spec: + ports: + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: operator-controller +--- +# Source: olmv1/templates/e2e/pod-olmv1-system-e2e-coverage-copy-pod.yml +apiVersion: v1 +kind: Pod +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: e2e + app.kubernetes.io/part-of: olm + name: e2e-coverage-copy-pod + namespace: olmv1-system +spec: + containers: + - command: + - sleep + - infinity + image: busybox:1.36 + name: tar + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /e2e-coverage + name: e2e-coverage-volume + readOnly: true + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + volumes: + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + readOnly: true +--- +# Source: olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + minReadySeconds: 5 + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: catalogd-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + control-plane: catalogd-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --leader-elect + - --metrics-bind-address=:7443 + - --external-address=catalogd-service.olmv1-system.svc + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --pull-cas-dir=/var/ca-certs + - --tls-profile=custom + - --tls-custom-version=TLSv1.3 + - --tls-custom-curves=X25519,prime256v1 + - --tls-custom-ciphers=TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384 + command: + - ./catalogd + env: + - name: GOCOVERDIR + value: /e2e-coverage + image: "quay.io/operator-framework/catalogd:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - mountPath: /e2e-coverage + name: e2e-coverage-volume + - mountPath: /var/cache/ + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: catalogd-controller-manager + volumes: + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogd-service-cert-git-version + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: catalogd-service-cert-git-version + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: operator-controller-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: operator-controller + control-plane: operator-controller-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8443 + - --leader-elect + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --pull-cas-dir=/var/ca-certs + - --tls-profile=modern + command: + - /operator-controller + env: + - name: GOCOVERDIR + value: /e2e-coverage + image: "quay.io/operator-framework/operator-controller:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /etc/containers + name: e2e-registries-conf + - mountPath: /e2e-coverage + name: e2e-coverage-volume + - mountPath: /var/cache + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: operator-controller-certs + readOnly: true + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: operator-controller-controller-manager + volumes: + - configMap: + name: e2e-registries-conf + name: e2e-registries-conf + - name: e2e-coverage-volume + persistentVolumeClaim: + claimName: e2e-coverage + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: operator-controller-cert + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca + namespace: cert-manager +spec: + commonName: olmv1-ca + isCA: true + issuerRef: + group: cert-manager.io + kind: Issuer + name: self-sign-issuer + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: olmv1-ca + secretTemplate: + annotations: + cert-manager.io/allow-direct-injection: "true" +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service-cert + namespace: olmv1-system +spec: + dnsNames: + - localhost + - catalogd-service.olmv1-system.svc + - catalogd-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: catalogd-service-cert-git-version +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: operator-controller-cert + namespace: olmv1-system +spec: + dnsNames: + - operator-controller-service.olmv1-system.svc + - operator-controller-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: operator-controller-cert +--- +# Source: olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca +spec: + ca: + secretName: olmv1-ca +--- +# Source: olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + annotations: + olm.operatorframework.io/feature-set: standard-e2e + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: self-sign-issuer + namespace: cert-manager +spec: + selfSigned: {} +--- +# Source: olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: catalogd-mutating-webhook-configuration + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + cert-manager.io/inject-ca-from-secret: cert-manager/olmv1-ca + olm.operatorframework.io/feature-set: standard-e2e +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: catalogd-service + namespace: olmv1-system + path: /mutate-olm-operatorframework-io-v1-clustercatalog + port: 9443 + failurePolicy: Fail + name: inject-metadata-name.olm.operatorframework.io + rules: + - apiGroups: + - olm.operatorframework.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clustercatalogs + sideEffects: None + timeoutSeconds: 10 + matchConditions: + - name: MissingOrIncorrectMetadataNameLabel + expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" diff --git a/manifests/standard.yaml b/manifests/standard.yaml new file mode 100644 index 0000000000..cf7eb0ee5c --- /dev/null +++ b/manifests/standard.yaml @@ -0,0 +1,2133 @@ +--- +# Source: olmv1/templates/namespace.yml +apiVersion: v1 +kind: Namespace +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + app.kubernetes.io/part-of: olm + name: olmv1-system +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-catalogd-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 7443 + protocol: TCP + - port: 8443 + protocol: TCP + - port: 9443 + protocol: TCP + podSelector: + matchLabels: + control-plane: catalogd-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-default-deny-all-traffic.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: default-deny-all-traffic + namespace: olmv1-system +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/networkpolicy/networkpolicy-olmv1-system-operator-controller-controller-manager.yml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + egress: + - {} + ingress: + - ports: + - port: 8443 + protocol: TCP + podSelector: + matchLabels: + control-plane: operator-controller-controller-manager + policyTypes: + - Ingress + - Egress +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/serviceaccount-olmv1-system-common-controller-manager.yml +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/crds/customresourcedefinition-clustercatalogs.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard + name: clustercatalogs.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterCatalog + listKind: ClusterCatalogList + plural: clustercatalogs + singular: clustercatalog + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.lastUnpacked + name: LastUnpacked + type: date + - jsonPath: .status.conditions[?(@.type=="Serving")].status + name: Serving + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec is the desired state of the ClusterCatalog. + spec is required. + The controller will work to ensure that the desired + catalog is unpacked and served over the catalog content HTTP server. + properties: + availabilityMode: + default: Available + description: |- + availabilityMode allows users to define how the ClusterCatalog is made available to clients on the cluster. + availabilityMode is optional. + + Allowed values are "Available" and "Unavailable" and omitted. + + When omitted, the default value is "Available". + + When set to "Available", the catalog contents will be unpacked and served over the catalog content HTTP server. + Setting the availabilityMode to "Available" tells clients that they should consider this ClusterCatalog + and its contents as usable. + + When set to "Unavailable", the catalog contents will no longer be served over the catalog content HTTP server. + When set to this availabilityMode it should be interpreted the same as the ClusterCatalog not existing. + Setting the availabilityMode to "Unavailable" can be useful in scenarios where a user may not want + to delete the ClusterCatalog all together, but would still like it to be treated as if it doesn't exist. + enum: + - Unavailable + - Available + type: string + priority: + default: 0 + description: |- + priority allows the user to define a priority for a ClusterCatalog. + priority is optional. + + A ClusterCatalog's priority is used by clients as a tie-breaker between ClusterCatalogs that meet the client's requirements. + A higher number means higher priority. + + It is up to clients to decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + When deciding how to break the tie in this scenario, it is recommended that clients prompt their users for additional input. + + When omitted, the default priority is 0 because that is the zero value of integers. + + Negative numbers can be used to specify a priority lower than the default. + Positive numbers can be used to specify a priority higher than the default. + + The lowest possible value is -2147483648. + The highest possible value is 2147483647. + format: int32 + type: integer + source: + description: |- + source allows a user to define the source of a catalog. + A "catalog" contains information on content that can be installed on a cluster. + Providing a catalog source makes the contents of the catalog discoverable and usable by + other on-cluster components. + These on-cluster components may do a variety of things with this information, such as + presenting the content in a GUI dashboard or installing content from the catalog on the cluster. + The catalog source must contain catalog metadata in the File-Based Catalog (FBC) format. + For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs. + source is a required field. + + Below is a minimal example of a ClusterCatalogSpec that sources a catalog from an image: + + source: + type: Image + image: + ref: quay.io/operatorhubio/catalog:latest + properties: + image: + description: |- + image is used to configure how catalog contents are sourced from an OCI image. + This field is required when type is Image, and forbidden otherwise. + properties: + pollIntervalMinutes: + description: |- + pollIntervalMinutes allows the user to set the interval, in minutes, at which the image source should be polled for new content. + pollIntervalMinutes is optional. + pollIntervalMinutes can not be specified when ref is a digest-based reference. + + When omitted, the image will not be polled for new content. + minimum: 1 + type: integer + ref: + description: |- + ref allows users to define the reference to a container image containing Catalog contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/catalog", "catalog", "my-catalog.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/catalog". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + x-kubernetes-validations: + - message: cannot specify pollIntervalMinutes while using digest-based + image + rule: 'self.ref.find(''(@.*:)'') != "" ? !has(self.pollIntervalMinutes) + : true' + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When using an image source, the image field must be set and must be the only field defined for this type. + enum: + - Image + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + required: + - source + type: object + status: + description: |- + status contains information about the state of the ClusterCatalog such as: + - Whether or not the catalog contents are being served via the catalog content HTTP server + - Whether or not the ClusterCatalog is progressing to a new state + - A reference to the source from which the catalog contents were retrieved + properties: + conditions: + description: |- + conditions is a representation of the current state for this ClusterCatalog. + + The current condition types are Serving and Progressing. + + The Serving condition is used to represent whether or not the contents of the catalog is being served via the HTTP(S) web server. + When it has a status of True and a reason of Available, the contents of the catalog are being served. + When it has a status of False and a reason of Unavailable, the contents of the catalog are not being served because the contents are not yet available. + When it has a status of False and a reason of UserSpecifiedUnavailable, the contents of the catalog are not being served because the catalog has been intentionally marked as unavailable. + + The Progressing condition is used to represent whether or not the ClusterCatalog is progressing or is ready to progress towards a new state. + When it has a status of True and a reason of Retrying, there was an error in the progression of the ClusterCatalog that may be resolved on subsequent reconciliation attempts. + When it has a status of True and a reason of Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + When it has a status of False and a reason of Blocked, there was an error in the progression of the ClusterCatalog that requires manual intervention for recovery. + + In the case that the Serving condition is True with reason Available and Progressing is True with reason Retrying, the previously fetched + catalog contents are still being served via the HTTP(S) web server while we are progressing towards serving a new version of the catalog + contents. This could occur when we've initially fetched the latest contents from the source for this catalog and when polling for changes + to the contents we identify that there are updates to the contents. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastUnpacked: + description: |- + lastUnpacked represents the last time the contents of the + catalog were extracted from their source format. As an example, + when using an Image source, the OCI image will be pulled and the + image layers written to a file-system backed cache. We refer to the + act of this extraction from the source format as "unpacking". + format: date-time + type: string + resolvedSource: + description: resolvedSource contains information about the resolved + source based on the source type. + properties: + image: + description: |- + image is a field containing resolution information for a catalog sourced from an image. + This field must be set when type is Image, and forbidden otherwise. + properties: + ref: + description: |- + ref contains the resolved image digest-based reference. + The digest format is used so users can use other tooling to fetch the exact + OCI manifests that were used to extract the catalog contents. + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest + rule: self.find('(@.*:)') != "" + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object + type: + description: |- + type is a reference to the type of source the catalog is sourced from. + type is required. + + The only allowed value is "Image". + + When set to "Image", information about the resolved image source will be set in the 'image' field. + enum: + - Image + type: string + required: + - image + - type + type: object + x-kubernetes-validations: + - message: image is required when source type is Image, and forbidden + otherwise + rule: 'has(self.type) && self.type == ''Image'' ? has(self.image) + : !has(self.image)' + urls: + description: urls contains the URLs that can be used to access the + catalog. + properties: + base: + description: |- + base is a cluster-internal URL that provides endpoints for + accessing the content of the catalog. + + It is expected that clients append the path for the endpoint they wish + to access. + + Currently, only a single endpoint is served and is accessible at the path + /api/v1. + + The endpoints served for the v1 API are: + - /all - this endpoint returns the entirety of the catalog contents in the FBC format + + As the needs of users and clients of the evolve, new endpoints may be added. + maxLength: 525 + type: string + x-kubernetes-validations: + - message: must be a valid URL + rule: isURL(self) + - message: scheme must be either http or https + rule: 'isURL(self) ? (url(https://github.com/self).getScheme() == "http" || url(https://github.com/self).getScheme() + == "https") : true' + required: + - base + type: object + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/crds/customresourcedefinition-clusterextensions.olm.operatorframework.io.yml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + olm.operatorframework.io/generator: standard + name: clusterextensions.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: ClusterExtension + listKind: ClusterExtensionList + plural: clusterextensions + singular: clusterextension + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.install.bundle.name + name: Installed Bundle + type: string + - jsonPath: .status.install.bundle.version + name: Version + type: string + - jsonPath: .status.conditions[?(@.type=='Installed')].status + name: Installed + type: string + - jsonPath: .status.conditions[?(@.type=='Progressing')].status + name: Progressing + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ClusterExtension is the Schema for the clusterextensions API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec is an optional field that defines the desired state + of the ClusterExtension. + properties: + config: + description: |- + config is an optional field used to specify bundle specific configuration + used to configure the bundle. Configuration is bundle specific and a bundle may provide + a configuration schema. When not specified, the default configuration of the resolved bundle will be used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the final manifests will be derived on a best-effort basis. More information on how + to configure the bundle should be found in its end-user documentation. + properties: + configType: + description: |- + configType is a required reference to the type of configuration source. + + Allowed values are "Inline" + + When this field is set to "Inline", the cluster extension configuration is defined inline within the + ClusterExtension resource. + enum: + - Inline + type: string + inline: + description: |- + inline contains JSON or YAML values specified directly in the + ClusterExtension. + + inline must be set if configType is 'Inline'. + inline accepts arbitrary JSON/YAML objects. + inline is validation at runtime against the schema provided by the bundle if a schema is provided. + type: object + x-kubernetes-preserve-unknown-fields: true + required: + - configType + type: object + x-kubernetes-validations: + - message: inline is required when configType is Inline, and forbidden + otherwise + rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) + : !has(self.inline)' + install: + description: |- + install is an optional field used to configure the installation options + for the ClusterExtension such as the pre-flight check configuration. + properties: + preflight: + description: |- + preflight is an optional field that can be used to configure the checks that are + run before installation or upgrade of the content for the package specified in the packageName field. + + When specified, it replaces the default preflight configuration for install/upgrade actions. + When not specified, the default configuration will be used. + properties: + crdUpgradeSafety: + description: |- + crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight + checks that run prior to upgrades of installed content. + + The CRD Upgrade Safety pre-flight check safeguards from unintended + consequences of upgrading a CRD, such as data loss. + properties: + enforcement: + description: |- + enforcement is a required field, used to configure the state of the CRD Upgrade Safety pre-flight check. + + Allowed values are "None" or "Strict". The default value is "Strict". + + When set to "None", the CRD Upgrade Safety pre-flight check will be skipped + when performing an upgrade operation. This should be used with caution as + unintended consequences such as data loss can occur. + + When set to "Strict", the CRD Upgrade Safety pre-flight check will be run when + performing an upgrade operation. + enum: + - None + - Strict + type: string + required: + - enforcement + type: object + required: + - crdUpgradeSafety + type: object + x-kubernetes-validations: + - message: at least one of [crdUpgradeSafety] are required when + preflight is specified + rule: has(self.crdUpgradeSafety) + type: object + x-kubernetes-validations: + - message: at least one of [preflight] are required when install is + specified + rule: has(self.preflight) + namespace: + description: |- + namespace is a reference to a Kubernetes namespace. + This is the namespace in which the provided ServiceAccount must exist. + It also designates the default namespace where namespace-scoped resources + for the extension are applied to the cluster. + Some extensions may contain namespace-scoped resources to be applied in other namespaces. + This namespace must exist. + + namespace is required, immutable, and follows the DNS label standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters or hyphens (-), + start and end with an alphanumeric character, and be no longer than 63 characters + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 63 + type: string + x-kubernetes-validations: + - message: namespace is immutable + rule: self == oldSelf + - message: namespace must be a valid DNS1123 label + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + serviceAccount: + description: |- + serviceAccount is a reference to a ServiceAccount used to perform all interactions + with the cluster that are required to manage the extension. + The ServiceAccount must be configured with the necessary permissions to perform these interactions. + The ServiceAccount must exist in the namespace referenced in the spec. + serviceAccount is required. + properties: + name: + description: |- + name is a required, immutable reference to the name of the ServiceAccount + to be used for installation and management of the content for the package + specified in the packageName field. + + This ServiceAccount must exist in the installNamespace. + + name follows the DNS subdomain standard as defined in [RFC 1123]. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-serviceaccount + - 123-serviceaccount + - 1-serviceaccount-2 + - someserviceaccount + - some.serviceaccount + + Some examples of invalid values are: + - -some-serviceaccount + - some-serviceaccount- + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must be a valid DNS1123 subdomain. It must contain + only lowercase alphanumeric characters, hyphens (-) or periods + (.), start and end with an alphanumeric character, and be + no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + required: + - name + type: object + source: + description: |- + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. + This field is required when sourceType is "Catalog", and forbidden otherwise. + properties: + channels: + description: |- + channels is an optional reference to a set of channels belonging to + the package specified in the packageName field. + + A "channel" is a package-author-defined stream of updates for an extension. + + Each channel in the list must follow the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. No more than 256 channels can be specified. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + - original_40 + - --default-channel + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + items: + maxLength: 253 + type: string + x-kubernetes-validations: + - message: channels entries must be valid DNS1123 subdomains + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + maxItems: 256 + type: array + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + packageName is required, immutable, and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + + [RFC 1123]: https://tools.ietf.org/html/rfc1123 + maxLength: 253 + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: CatalogProvided + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + + When this field is set to "CatalogProvided", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "SelfCertified", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + When this field is omitted, the default value is "CatalogProvided". + enum: + - CatalogProvided + - SelfCertified + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + type: string + x-kubernetes-validations: + - message: invalid version expression + rule: self.matches("^(\\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|[x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*]))?(\\.(0|[1-9]\\d*|x|X|\\*))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)((?:\\s+|,\\s*|\\s*\\|\\|\\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\\^)\\s*(v?(0|[1-9]\\d*|x|X|\\*])(\\.(0|[1-9]\\d*|x|X|\\*))?(\\.(0|[1-9]\\d*|x|X|\\*]))?(-([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?(\\+([0-9A-Za-z\\-]+(\\.[0-9A-Za-z\\-]+)*))?)\\s*)*$") + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are "Catalog" + + When this field is set to "Catalog", information for determining the + appropriate bundle of content to install will be fetched from + ClusterCatalog resources existing on the cluster. + When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: catalog is required when sourceType is Catalog, and forbidden + otherwise + rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ? + has(self.catalog) : !has(self.catalog)' + required: + - namespace + - serviceAccount + - source + type: object + status: + description: status is an optional field that defines the observed state + of the ClusterExtension. + properties: + conditions: + description: |- + The set of condition types which apply to all spec.source variations are Installed and Progressing. + + The Installed condition represents whether or not the bundle has been installed for this ClusterExtension. + When Installed is True and the Reason is Succeeded, the bundle has been successfully installed. + When Installed is False and the Reason is Failed, the bundle has failed to install. + + The Progressing condition represents whether or not the ClusterExtension is advancing towards a new state. + When Progressing is True and the Reason is Succeeded, the ClusterExtension is making progress towards a new state. + When Progressing is True and the Reason is Retrying, the ClusterExtension has encountered an error that could be resolved on subsequent reconciliation attempts. + When Progressing is False and the Reason is Blocked, the ClusterExtension has encountered an error that requires manual intervention for recovery. + + When the ClusterExtension is sourced from a catalog, if may also communicate a deprecation condition. + These are indications from a package owner to guide users away from a particular package, channel, or bundle. + BundleDeprecated is set if the requested bundle version is marked deprecated in the catalog. + ChannelDeprecated is set if the requested channel is marked deprecated in the catalog. + PackageDeprecated is set if the requested package is marked deprecated in the catalog. + Deprecated is a rollup condition that is present when any of the deprecated conditions are present. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + install: + description: install is a representation of the current installation + status for this ClusterExtension. + properties: + bundle: + description: |- + bundle is a required field which represents the identifying attributes of a bundle. + + A "bundle" is a versioned set of content that represents the resources that + need to be applied to a cluster to install a package. + properties: + name: + description: |- + name is required and follows the DNS subdomain standard + as defined in [RFC 1123]. It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric character, + and be no longer than 253 characters. + type: string + x-kubernetes-validations: + - message: packageName must be a valid DNS1123 subdomain. + It must contain only lowercase alphanumeric characters, + hyphens (-) or periods (.), start and end with an alphanumeric + character, and be no longer than 253 characters + rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + version: + description: |- + version is a required field and is a reference to the version that this bundle represents + version follows the semantic versioning standard as defined in https://semver.org/. + type: string + x-kubernetes-validations: + - message: version must be well-formed semver + rule: self.matches("^([0-9]+)(\\.[0-9]+)?(\\.[0-9]+)?(-([-0-9A-Za-z]+(\\.[-0-9A-Za-z]+)*))?(\\+([-0-9A-Za-z]+(-\\.[-0-9A-Za-z]+)*))?") + required: + - name + - version + type: object + required: + - bundle + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +# Source: olmv1/templates/rbac/clusterrole-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: catalogd-manager-role + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs/status + verbs: + - get + - patch + - update +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-metrics-reader.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-common-proxy-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-clusterextension-viewer-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-clusterextension-viewer-role +rules: + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrole-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-controller-manager-role + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard +rules: + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - apiGroups: + - olm.operatorframework.io + resources: + - clustercatalogs + verbs: + - get + - list + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/finalizers + verbs: + - update + - apiGroups: + - olm.operatorframework.io + resources: + - clusterextensions/status + verbs: + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - list + - watch +--- +# Source: olmv1/templates/rbac/clusterrolebinding-catalogd-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: catalogd-proxy-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-common-proxy-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-proxy-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/clusterrolebinding-operator-controller-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/role-olmv1-system-catalogd-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: catalogd-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard +rules: + - apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-common-leader-election-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-role + namespace: olmv1-system +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +# Source: olmv1/templates/rbac/role-olmv1-system-operator-controller-manager-role.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: operator-controller-manager-role + namespace: olmv1-system + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + annotations: + olm.operatorframework.io/feature-set: standard +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-leader-election-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-leader-election-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-leader-election-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-leader-election-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-manager-role +subjects: + - kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/rbac/rolebinding-olmv1-system-common-manager-rolebinding.yml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-controller-manager-role +subjects: + - kind: ServiceAccount + name: operator-controller-controller-manager + namespace: olmv1-system +--- +# Source: olmv1/templates/service-olmv1-system-catalogd-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service + namespace: olmv1-system +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + - name: webhook + port: 9443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 7443 + protocol: TCP + targetPort: 7443 + selector: + app.kubernetes.io/name: catalogd +--- +# Source: olmv1/templates/service-olmv1-system-operator-controller-service.yml +apiVersion: v1 +kind: Service +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-service + namespace: olmv1-system +spec: + ports: + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app.kubernetes.io/name: operator-controller +--- +# Source: olmv1/templates/deployment-olmv1-system-catalogd-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-controller-manager + namespace: olmv1-system +spec: + minReadySeconds: 5 + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: catalogd-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + control-plane: catalogd-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --leader-elect + - --metrics-bind-address=:7443 + - --external-address=catalogd-service.olmv1-system.svc + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --pull-cas-dir=/var/ca-certs + command: + - ./catalogd + image: "quay.io/operator-framework/catalogd:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - mountPath: /var/cache/ + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: catalogserver-certs + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: catalogd-controller-manager + volumes: + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: catalogserver-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: catalogd-service-cert-git-version + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: catalogd-service-cert-git-version + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/deployment-olmv1-system-operator-controller-controller-manager.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kubectl.kubernetes.io/default-logs-container: manager + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + app.kubernetes.io/part-of: olm + name: operator-controller-controller-manager + namespace: olmv1-system +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # Allow temporary 2 pods (1 + 1) for zero-downtime updates + maxUnavailable: 0 # Never allow pods to be unavailable during updates + selector: + matchLabels: + control-plane: operator-controller-controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: operator-controller + control-plane: operator-controller-controller-manager + app.kubernetes.io/part-of: olm + spec: + containers: + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=:8443 + - --leader-elect + - --tls-cert=/var/certs/tls.crt + - --tls-key=/var/certs/tls.key + - --catalogd-cas-dir=/var/ca-certs + - --pull-cas-dir=/var/ca-certs + command: + - /operator-controller + image: "quay.io/operator-framework/operator-controller:devel" + name: manager + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /var/cache + name: cache + - mountPath: /tmp + name: tmp + - mountPath: /var/certs + name: operator-controller-certs + readOnly: true + - mountPath: /var/ca-certs + name: ca-certs + readOnly: true + imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + terminationMessagePolicy: FallbackToLogsOnError + serviceAccountName: operator-controller-controller-manager + volumes: + - emptyDir: {} + name: cache + - emptyDir: {} + name: tmp + - name: operator-controller-certs + secret: + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + optional: false + secretName: operator-controller-cert + - name: ca-certs + secret: + items: + - key: ca.crt + path: olm-ca.crt + optional: false + secretName: operator-controller-cert + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - arm64 + - ppc64le + - s390x + - key: kubernetes.io/os + operator: In + values: + - linux + nodeSelector: + kubernetes.io/os: linux + node-role.kubernetes.io/control-plane: "" + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 10 + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/control-plane + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 120 + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 120 +--- +# Source: olmv1/templates/cert-manager/certificate-cert-manager-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca + namespace: cert-manager +spec: + commonName: olmv1-ca + isCA: true + issuerRef: + group: cert-manager.io + kind: Issuer + name: self-sign-issuer + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: olmv1-ca + secretTemplate: + annotations: + cert-manager.io/allow-direct-injection: "true" +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-catalogd-service-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-service-cert + namespace: olmv1-system +spec: + dnsNames: + - localhost + - catalogd-service.olmv1-system.svc + - catalogd-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: catalogd-service-cert-git-version +--- +# Source: olmv1/templates/cert-manager/certificate-olmv1-system-operator-controller-cert.yml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: operator-controller-cert + namespace: olmv1-system +spec: + dnsNames: + - operator-controller-service.olmv1-system.svc + - operator-controller-service.olmv1-system.svc.cluster.local + issuerRef: + group: cert-manager.io + kind: ClusterIssuer + name: olmv1-ca + privateKey: + algorithm: ECDSA + rotationPolicy: Always + size: 256 + secretName: operator-controller-cert +--- +# Source: olmv1/templates/cert-manager/clusterissuer-olmv1-ca.yml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: olmv1-ca +spec: + ca: + secretName: olmv1-ca +--- +# Source: olmv1/templates/cert-manager/issuer-cert-manager-self-sign-issuer.yml +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + annotations: + olm.operatorframework.io/feature-set: standard + labels: + app.kubernetes.io/name: olmv1 + app.kubernetes.io/part-of: olm + name: self-sign-issuer + namespace: cert-manager +spec: + selfSigned: {} +--- +# Source: olmv1/templates/mutatingwebhookconfiguration-catalogd-mutating-webhook-configuration.yml +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: catalogd-mutating-webhook-configuration + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + annotations: + cert-manager.io/inject-ca-from-secret: cert-manager/olmv1-ca + olm.operatorframework.io/feature-set: standard +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: catalogd-service + namespace: olmv1-system + path: /mutate-olm-operatorframework-io-v1-clustercatalog + port: 9443 + failurePolicy: Fail + name: inject-metadata-name.olm.operatorframework.io + rules: + - apiGroups: + - olm.operatorframework.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clustercatalogs + sideEffects: None + timeoutSeconds: 10 + matchConditions: + - name: MissingOrIncorrectMetadataNameLabel + expression: "'name' in object.metadata && (!has(object.metadata.labels) || !('olm.operatorframework.io/metadata.name' in object.metadata.labels) || object.metadata.labels['olm.operatorframework.io/metadata.name'] != object.metadata.name)" diff --git a/mkdocs.yml b/mkdocs.yml index f7b20ae070..e891896222 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,8 +49,7 @@ nav: - Content Resolution: concepts/controlling-catalog-selection.md - Version Ranges: concepts/version-ranges.md - API Reference: - - Operator Controller API reference: api-reference/operator-controller-api-reference.md - - CatalogD API reference: api-reference/catalogd-api-reference.md + - OLMv1 API reference: api-reference/olmv1-api-reference.md - CatalogD Web Server reference: api-reference/catalogd-webserver.md - Contribute: - Contributing: contribute/contributing.md diff --git a/requirements.txt b/requirements.txt index 737166714b..de4e5f9830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,35 @@ Babel==2.17.0 -beautifulsoup4==4.13.4 -certifi==2025.4.26 -charset-normalizer==3.4.2 -click==8.1.8 +beautifulsoup4==4.14.2 +certifi==2025.10.5 +charset-normalizer==3.4.4 +click==8.3.0 colorama==0.4.6 cssselect==1.3.0 ghp-import==2.1.0 -idna==3.10 +idna==3.11 Jinja2==3.1.6 -lxml==5.4.0 -Markdown==3.8 -markdown2==2.5.3 -MarkupSafe==3.0.2 +lxml==6.0.2 +Markdown==3.9 +markdown2==2.5.4 +MarkupSafe==3.0.3 mergedeep==1.3.4 mkdocs==1.6.1 -mkdocs-material==9.6.14 +mkdocs-material==9.6.22 mkdocs-material-extensions==1.3.1 packaging==25.0 paginate==0.5.7 pathspec==0.12.1 -platformdirs==4.3.8 -Pygments==2.19.1 -pymdown-extensions==10.15 +platformdirs==4.5.0 +Pygments==2.19.2 +pymdown-extensions==10.16.1 pyquery==2.0.1 python-dateutil==2.9.0.post0 -PyYAML==6.0.2 +PyYAML==6.0.3 pyyaml_env_tag==1.1 readtime==3.0.0 -regex==2024.11.6 -requests==2.32.3 +regex==2025.10.23 +requests==2.32.5 six==1.17.0 -soupsieve==2.7 -urllib3==2.4.0 +soupsieve==2.8 +urllib3==2.5.0 watchdog==6.0.0 diff --git a/scripts/OWNERS b/scripts/OWNERS new file mode 100644 index 0000000000..b44dad0ea8 --- /dev/null +++ b/scripts/OWNERS @@ -0,0 +1,2 @@ +approvers: + - manifest-approvers diff --git a/scripts/install.tpl.sh b/scripts/install.tpl.sh index 8088a25158..2ddf79856b 100644 --- a/scripts/install.tpl.sh +++ b/scripts/install.tpl.sh @@ -3,6 +3,39 @@ set -euo pipefail IFS=$'\n\t' olmv1_manifest=$MANIFEST +olmv1_namespace=olmv1-system + +usage() { + cmd=$(basename $0) + cat <] [-h] + +DESCRIPTION + Installs OLMv1 in the provided with cert-manager. + A kubernetes configuration must already be present. + + -n + install OLMv1 in the given . Defaults to olmv1-system. + + -h + help (this text) +EOF + exit 0 +} + + +while getopts n:h opt; do + case ${opt} in + n) olmv1_namespace=${OPTARG} ;; + h) usage ;; + *) echo "Unknown option" >&2 + exit 1 + esac +done if [[ -z "$olmv1_manifest" ]]; then echo "Error: Missing required MANIFEST variable" @@ -69,11 +102,16 @@ kubectl_wait "cert-manager" "deployment/cert-manager" "60s" kubectl_wait_for_query "mutatingwebhookconfigurations/cert-manager-webhook" '{.webhooks[0].clientConfig.caBundle}' 60 5 kubectl_wait_for_query "validatingwebhookconfigurations/cert-manager-webhook" '{.webhooks[0].clientConfig.caBundle}' 60 5 -kubectl apply -f "${olmv1_manifest}" +# Change the file into a file:// url +if [ -f "${olmv1_manifest}" ]; then + olmv1_manifest=file://localhost$(realpath ${olmv1_manifest}) +fi + +curl -L -s "${olmv1_manifest}" | sed "s/olmv1-system/${olmv1_namespace}/g" | kubectl apply -f - # Wait for the rollout, and then wait for the deployment to be Available -kubectl_wait_rollout "olmv1-system" "deployment/catalogd-controller-manager" "60s" -kubectl_wait "olmv1-system" "deployment/catalogd-controller-manager" "60s" -kubectl_wait "olmv1-system" "deployment/operator-controller-controller-manager" "60s" +kubectl_wait_rollout "${olmv1_namespace}" "deployment/catalogd-controller-manager" "60s" +kubectl_wait "${olmv1_namespace}" "deployment/catalogd-controller-manager" "60s" +kubectl_wait "${olmv1_namespace}" "deployment/operator-controller-controller-manager" "60s" if [[ "${install_default_catalogs}" != "false" ]]; then kubectl apply -f "${default_catalogs_manifest}" diff --git a/test/convert/README.md b/test/convert/README.md deleted file mode 100644 index 590c65730b..0000000000 --- a/test/convert/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## registry+v1 bundle generation regression tests - -This directory includes test cases for the rukpak/convert package based on real bundle data. -The manifests are generated and manually/visually verified for correctness. - -The `generate-manifests.go` tool is used to generate the tests cases by calling convert.Convert on bundles -in the `testdata` directory. diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index 9d3ad82f7a..ab0bf48b1c 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -10,301 +10,29 @@ import ( "github.com/google/go-containerregistry/pkg/crane" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/utils/ptr" ocv1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/test/utils" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" + . "github.com/operator-framework/operator-controller/test/helpers" ) const ( - artifactName = "operator-controller-e2e" + artifactName = "operator-controller-e2e" + pollDuration = time.Minute + pollInterval = time.Second + testCatalogRefEnvVar = "CATALOG_IMG" + testCatalogName = "test-catalog" ) -var pollDuration = time.Minute -var pollInterval = time.Second - -func createNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - err := c.Create(ctx, ns) - if err != nil { - return nil, err - } - return ns, nil -} - -func createServiceAccount(ctx context.Context, name types.NamespacedName, clusterExtensionName string) (*corev1.ServiceAccount, error) { - sa := &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: name.Name, - Namespace: name.Namespace, - }, - } - err := c.Create(ctx, sa) - if err != nil { - return nil, err - } - - return sa, createClusterRoleAndBindingForSA(ctx, name.Name, sa, clusterExtensionName) -} - -func createClusterRoleAndBindingForSA(ctx context.Context, name string, sa *corev1.ServiceAccount, clusterExtensionName string) error { - cr := &rbacv1.ClusterRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{ - "olm.operatorframework.io", - }, - Resources: []string{ - "clusterextensions/finalizers", - }, - Verbs: []string{ - "update", - }, - ResourceNames: []string{clusterExtensionName}, - }, - { - APIGroups: []string{ - "", - }, - Resources: []string{ - "configmaps", - "secrets", // for helm - "services", - "serviceaccounts", - }, - Verbs: []string{ - "create", - "update", - "delete", - "patch", - "get", - "list", - "watch", - }, - }, - { - APIGroups: []string{ - "apiextensions.k8s.io", - }, - Resources: []string{ - "customresourcedefinitions", - }, - Verbs: []string{ - "create", - "update", - "delete", - "patch", - "get", - "list", - "watch", - }, - }, - { - APIGroups: []string{ - "apps", - }, - Resources: []string{ - "deployments", - }, - Verbs: []string{ - "create", - "update", - "delete", - "patch", - "get", - "list", - "watch", - }, - }, - { - APIGroups: []string{ - "rbac.authorization.k8s.io", - }, - Resources: []string{ - "clusterroles", - "roles", - "clusterrolebindings", - "rolebindings", - }, - Verbs: []string{ - "create", - "update", - "delete", - "patch", - "get", - "list", - "watch", - "bind", - "escalate", - }, - }, - }, - } - err := c.Create(ctx, cr) - if err != nil { - return err - } - crb := &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: sa.Name, - Namespace: sa.Namespace, - }, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: name, - }, - } - err = c.Create(ctx, crb) - if err != nil { - return err - } - - return nil -} - -func testInit(t *testing.T) (*ocv1.ClusterExtension, *ocv1.ClusterCatalog, *corev1.ServiceAccount, *corev1.Namespace) { - var err error - - clusterExtensionName := fmt.Sprintf("clusterextension-%s", rand.String(8)) - - ns, err := createNamespace(context.Background(), clusterExtensionName) - require.NoError(t, err) - - clusterExtension := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterExtensionName, - }, - } - - extensionCatalog, err := createTestCatalog(context.Background(), testCatalogName, os.Getenv(testCatalogRefEnvVar)) - require.NoError(t, err) - - name := types.NamespacedName{ - Name: clusterExtensionName, - Namespace: ns.GetName(), - } - - sa, err := createServiceAccount(context.Background(), name, clusterExtensionName) - require.NoError(t, err) - - validateCatalogUnpack(t) - - return clusterExtension, extensionCatalog, sa, ns -} - -func validateCatalogUnpack(t *testing.T) { - catalog := &ocv1.ClusterCatalog{} - t.Log("Ensuring ClusterCatalog has Status.Condition of Progressing with a status == True and reason == Succeeded") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - err := c.Get(context.Background(), types.NamespacedName{Name: testCatalogName}, catalog) - assert.NoError(ct, err) - cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeProgressing) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("Checking that catalog has the expected metadata label") - assert.NotNil(t, catalog.ObjectMeta.Labels) - assert.Contains(t, catalog.ObjectMeta.Labels, "olm.operatorframework.io/metadata.name") - assert.Equal(t, testCatalogName, catalog.ObjectMeta.Labels["olm.operatorframework.io/metadata.name"]) - - t.Log("Ensuring ClusterCatalog has Status.Condition of Type = Serving with status == True") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - err := c.Get(context.Background(), types.NamespacedName{Name: testCatalogName}, catalog) - assert.NoError(ct, err) - cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeServing) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonAvailable, cond.Reason) - }, pollDuration, pollInterval) -} - -func ensureNoExtensionResources(t *testing.T, clusterExtensionName string) { - ls := labels.Set{"olm.operatorframework.io/owner-name": clusterExtensionName} - - // CRDs may take an extra long time to be deleted, and may run into the following error: - // Condition=Terminating Status=True Reason=InstanceDeletionFailed Message="could not list instances: storage is (re)initializing" - t.Logf("By waiting for CustomResourceDefinitions of %q to be deleted", clusterExtensionName) - require.EventuallyWithT(t, func(ct *assert.CollectT) { - list := &apiextensionsv1.CustomResourceDefinitionList{} - err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) - assert.NoError(ct, err) - assert.Empty(ct, list.Items) - }, 5*pollDuration, pollInterval) - - t.Logf("By waiting for ClusterRoleBindings of %q to be deleted", clusterExtensionName) - require.EventuallyWithT(t, func(ct *assert.CollectT) { - list := &rbacv1.ClusterRoleBindingList{} - err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) - assert.NoError(ct, err) - assert.Empty(ct, list.Items) - }, 2*pollDuration, pollInterval) - - t.Logf("By waiting for ClusterRoles of %q to be deleted", clusterExtensionName) - require.EventuallyWithT(t, func(ct *assert.CollectT) { - list := &rbacv1.ClusterRoleList{} - err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) - assert.NoError(ct, err) - assert.Empty(ct, list.Items) - }, 2*pollDuration, pollInterval) -} - -func testCleanup(t *testing.T, cat *ocv1.ClusterCatalog, clusterExtension *ocv1.ClusterExtension, sa *corev1.ServiceAccount, ns *corev1.Namespace) { - t.Logf("By deleting ClusterCatalog %q", cat.Name) - require.NoError(t, c.Delete(context.Background(), cat)) - require.Eventually(t, func() bool { - err := c.Get(context.Background(), types.NamespacedName{Name: cat.Name}, &ocv1.ClusterCatalog{}) - return errors.IsNotFound(err) - }, pollDuration, pollInterval) - - t.Logf("By deleting ClusterExtension %q", clusterExtension.Name) - require.NoError(t, c.Delete(context.Background(), clusterExtension)) - require.Eventually(t, func() bool { - err := c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, &ocv1.ClusterExtension{}) - return errors.IsNotFound(err) - }, pollDuration, pollInterval) - - t.Logf("By deleting ServiceAccount %q", sa.Name) - require.NoError(t, c.Delete(context.Background(), sa)) - require.Eventually(t, func() bool { - err := c.Get(context.Background(), types.NamespacedName{Name: sa.Name, Namespace: sa.Namespace}, &corev1.ServiceAccount{}) - return errors.IsNotFound(err) - }, pollDuration, pollInterval) - - ensureNoExtensionResources(t, clusterExtension.Name) - - t.Logf("By deleting Namespace %q", ns.Name) - require.NoError(t, c.Delete(context.Background(), ns)) - require.Eventually(t, func() bool { - err := c.Get(context.Background(), types.NamespacedName{Name: ns.Name}, &corev1.Namespace{}) - return errors.IsNotFound(err) - }, pollDuration, pollInterval) -} - func TestClusterExtensionInstallRegistry(t *testing.T) { type testCase struct { name string @@ -317,7 +45,7 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { }, { // NOTE: This test requires an extra configuration in /etc/containers/registries.conf, which is mounted - // for this e2e via the ./config/components/registries-conf kustomize component as part of the e2e overlay. + // for this e2e via the ./config/components/e2e/registries-conf kustomize component as part of the e2e component. // The goal here is to prove that "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" is // mapped to the "real" registry hostname ("docker-registry.operator-controller-e2e.svc.cluster.local:5000"). name: "package requires mirror registry configuration in /etc/containers/registries.conf", @@ -328,8 +56,8 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When the extension bundle format is registry+v1") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -353,30 +81,40 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) }, pollDuration, pollInterval) t.Log("By eventually reporting progressing as True") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) t.Log("By eventually installing the package successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + }, pollDuration, pollInterval) + + t.Log("By eventually creating the NetworkPolicy named 'test-operator-network-policy'") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + var np networkingv1.NetworkPolicy + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: "test-operator-network-policy", Namespace: ns.Name}, &np)) }, pollDuration, pollInterval) + + t.Log("By verifying that no templating occurs for registry+v1 bundle manifests") + cm := corev1.ConfigMap{} + require.NoError(t, c.Get(context.Background(), types.NamespacedName{Namespace: ns.Name, Name: "test-configmap"}, &cm)) + require.Contains(t, cm.Annotations, "shouldNotTemplate") + require.Contains(t, cm.Annotations["shouldNotTemplate"], "{{ $labels.namespace }}") }) } } @@ -388,8 +126,8 @@ func TestClusterExtensionInstallRegistryDynamic(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When the extension bundle format is registry+v1") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -427,7 +165,7 @@ location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000"`, t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) }, 2*time.Minute, pollInterval) // Give the check 2 minutes instead of the typical 1 for the pod's @@ -436,35 +174,33 @@ location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000"`, // ConfigMap cache TTL of 1 minute = 2 minutes t.Log("By eventually reporting progressing as True") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, 2*time.Minute, pollInterval) t.Log("By eventually installing the package successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) }, pollDuration, pollInterval) } func TestClusterExtensionInstallRegistryMultipleBundles(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - extraCatalog, err := createTestCatalog(context.Background(), "extra-test-catalog", os.Getenv(testCatalogRefEnvVar)) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + extraCatalog, err := CreateTestCatalog(context.Background(), "extra-test-catalog", os.Getenv(testCatalogRefEnvVar)) require.NoError(t, err) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) defer func(cat *ocv1.ClusterCatalog) { require.NoError(t, c.Delete(context.Background(), cat)) @@ -492,18 +228,17 @@ func TestClusterExtensionInstallRegistryMultipleBundles(t *testing.T) { t.Log("By eventually reporting a failed resolution with multiple bundles") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) }, pollDuration, pollInterval) t.Log("By eventually reporting Progressing == True and Reason Retrying") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonRetrying, cond.Reason) - assert.Contains(ct, cond.Message, "in multiple catalogs with the same priority [extra-test-catalog test-catalog]") - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, "in multiple catalogs with the same priority [extra-test-catalog test-catalog]") }, pollDuration, pollInterval) } @@ -511,8 +246,8 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When resolving upgrade edges") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) t.Log("By creating an ClusterExtension at a specified version") @@ -533,8 +268,8 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful installation") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - assert.Equal(ct, + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.Equal(ct, &ocv1.ClusterExtensionInstallStatus{Bundle: ocv1.BundleMetadata{ Name: "test-operator.1.0.0", Version: "1.0.0", @@ -543,10 +278,9 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { ) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) t.Log("It does not allow to upgrade the ClusterExtension to a non-successor version") @@ -556,17 +290,16 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting an unsatisfiable resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) }, pollDuration, pollInterval) t.Log("By eventually reporting Progressing == True and Reason Retrying") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, ocv1.ReasonRetrying, cond.Reason) - assert.Equal(ct, "error upgrading from currently installed version \"1.0.0\": no bundles found for package \"test\" matching version \"1.2.0\"", cond.Message) - } + require.NotNil(ct, cond) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Equal(ct, "error upgrading from currently installed version \"1.0.0\": no bundles found for package \"test\" matching version \"1.2.0\"", cond.Message) }, pollDuration, pollInterval) } @@ -574,8 +307,8 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When resolving upgrade edges") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) t.Log("By creating an ClusterExtension at a specified version") @@ -595,12 +328,11 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) t.Log("It allows to upgrade the ClusterExtension to a non-successor version") @@ -611,20 +343,19 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting a satisfiable resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) } func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When resolving upgrade edges") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) t.Log("By creating an ClusterExtension at a specified version") @@ -644,12 +375,11 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) t.Log("It does allow to upgrade the ClusterExtension to any of the successor versions within non-zero major version") @@ -659,20 +389,19 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) } func TestClusterExtensionInstallReResolvesWhenCatalogIsPatched(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("It resolves again when a catalog is patched with new ImageRef") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -702,45 +431,36 @@ func TestClusterExtensionInstallReResolvesWhenCatalogIsPatched(t *testing.T) { t.Log("By reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) // patch imageRef tag on test-catalog image with v2 image t.Log("By patching the catalog ImageRef to point to the v2 catalog") - updatedCatalogImage := fmt.Sprintf("%s/e2e/test-catalog:v2", os.Getenv("LOCAL_REGISTRY_HOST")) + updatedCatalogImage := fmt.Sprintf("%s/e2e/test-catalog:v2", os.Getenv("CLUSTER_REGISTRY_HOST")) err := patchTestCatalog(context.Background(), testCatalogName, updatedCatalogImage) require.NoError(t, err) require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, extensionCatalog)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, extensionCatalog)) cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonAvailable, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) }, pollDuration, pollInterval) t.Log("By eventually installing the package successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - assert.Contains(ct, clusterExtension.Status.Install.Bundle.Version, "2.0.0") - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.Contains(ct, clusterExtension.Status.Install.Bundle.Version, "1.3.0") }, pollDuration, pollInterval) - - t.Log("By verifying that no templating occurs for registry+v1 bundle manifests") - cm := corev1.ConfigMap{} - require.NoError(t, c.Get(context.Background(), types.NamespacedName{Namespace: ns.Name, Name: "test-configmap"}, &cm)) - require.Contains(t, cm.Annotations, "shouldNotTemplate") - require.Contains(t, cm.Annotations["shouldNotTemplate"], "{{ $labels.namespace }}") } func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { @@ -749,13 +469,13 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { // Tag the image with the new tag var err error - v1Image := fmt.Sprintf("%s/%s", os.Getenv("CLUSTER_REGISTRY_HOST"), os.Getenv("E2E_TEST_CATALOG_V1")) + v1Image := fmt.Sprintf("%s/%s", os.Getenv("LOCAL_REGISTRY_HOST"), os.Getenv("E2E_TEST_CATALOG_V1")) err = crane.Tag(v1Image, latestImageTag, crane.Insecure) require.NoError(t, err) // create a test-catalog with latest image tag - latestCatalogImage := fmt.Sprintf("%s/e2e/test-catalog:latest", os.Getenv("LOCAL_REGISTRY_HOST")) - extensionCatalog, err := createTestCatalog(context.Background(), testCatalogName, latestCatalogImage) + latestCatalogImage := fmt.Sprintf("%s/e2e/test-catalog:latest", os.Getenv("CLUSTER_REGISTRY_HOST")) + extensionCatalog, err := CreateTestCatalog(context.Background(), testCatalogName, latestCatalogImage) require.NoError(t, err) clusterExtensionName := fmt.Sprintf("clusterextension-%s", rand.String(8)) clusterExtension := &ocv1.ClusterExtension{ @@ -763,11 +483,11 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { Name: clusterExtensionName, }, } - ns, err := createNamespace(context.Background(), clusterExtensionName) + ns, err := CreateNamespace(context.Background(), clusterExtensionName) require.NoError(t, err) - sa, err := createServiceAccount(context.Background(), types.NamespacedName{Name: clusterExtensionName, Namespace: ns.Name}, clusterExtensionName) + sa, err := CreateServiceAccount(context.Background(), types.NamespacedName{Name: clusterExtensionName, Namespace: ns.Name}, clusterExtensionName) require.NoError(t, err) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -791,44 +511,41 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { t.Log("By reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) // update tag on test-catalog image with v2 image t.Log("By updating the catalog tag to point to the v2 catalog") - v2Image := fmt.Sprintf("%s/%s", os.Getenv("CLUSTER_REGISTRY_HOST"), os.Getenv("E2E_TEST_CATALOG_V2")) + v2Image := fmt.Sprintf("%s/%s", os.Getenv("LOCAL_REGISTRY_HOST"), os.Getenv("E2E_TEST_CATALOG_V2")) err = crane.Tag(v2Image, latestImageTag, crane.Insecure) require.NoError(t, err) require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, extensionCatalog)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, extensionCatalog)) cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonAvailable, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) }, pollDuration, pollInterval) t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) } func TestClusterExtensionInstallReResolvesWhenManagedContentChanged(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("It resolves again when managed content is changed") - clusterExtension, extensionCatalog, sa, ns := testInit(t) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -852,13 +569,12 @@ func TestClusterExtensionInstallReResolvesWhenManagedContentChanged(t *testing.T t.Log("By reporting a successful installation") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") }, pollDuration, pollInterval) t.Log("By deleting a managed resource") @@ -872,26 +588,18 @@ func TestClusterExtensionInstallReResolvesWhenManagedContentChanged(t *testing.T t.Log("By eventually re-creating the managed resource") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: testConfigMap.Name, Namespace: testConfigMap.Namespace}, testConfigMap)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: testConfigMap.Name, Namespace: testConfigMap.Namespace}, testConfigMap)) }, pollDuration, pollInterval) } -func TestClusterExtensionRecoversFromInitialInstallFailedWhenFailureFixed(t *testing.T) { +func TestClusterExtensionRecoversFromNoNamespaceWhenFailureFixed(t *testing.T) { t.Log("When a cluster extension is installed from a catalog") t.Log("When the extension bundle format is registry+v1") - clusterExtension, extensionCatalog, _, ns := testInit(t) + t.Log("By not creating the Namespace and ServiceAccount") + clusterExtension, extensionCatalog := TestInitClusterExtensionClusterCatalog(t) - name := rand.String(10) - sa := &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns.Name, - }, - } - err := c.Create(context.Background(), sa) - require.NoError(t, err) - defer testCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer TestCleanup(t, extensionCatalog, clusterExtension, nil, nil) defer utils.CollectTestArtifacts(t, artifactName, c, cfg) clusterExtension.Spec = ocv1.ClusterExtensionSpec{ @@ -904,67 +612,180 @@ func TestClusterExtensionRecoversFromInitialInstallFailedWhenFailureFixed(t *tes }, }, }, - Namespace: ns.Name, + Namespace: clusterExtension.Name, ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, + Name: clusterExtension.Name, }, } + t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") require.NoError(t, c.Create(context.Background(), clusterExtension)) - t.Log("By eventually reporting a successful resolution and bundle path") + t.Log("By eventually reporting Progressing == True with Reason Retrying") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By eventually reporting Installed != True") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.NotEqual(ct, metav1.ConditionTrue, cond.Status) }, pollDuration, pollInterval) + t.Log("By creating the Namespace and ServiceAccount") + sa, ns := TestInitServiceAccountNamespace(t, clusterExtension.Name) + defer TestCleanup(t, nil, nil, sa, ns) + + // NOTE: In order to ensure predictable results we need to ensure we have a single + // known failure with a singular fix operation. Additionally, due to the exponential + // backoff of this eventually check we MUST ensure we do not touch the ClusterExtension + // after creating int the Namespace and ServiceAccount. + t.Log("By eventually installing the package successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotEmpty(ct, clusterExtension.Status.Install) + }, pollDuration, pollInterval) + + t.Log("By eventually reporting Progressing == True with Reason Success") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) +} + +func TestClusterExtensionRecoversFromExistingDeploymentWhenFailureFixed(t *testing.T) { + t.Log("When a cluster extension is installed from a catalog") + t.Log("When the extension bundle format is registry+v1") + + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + clusterExtension.Spec = ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, + }, + }, + Namespace: clusterExtension.Name, + ServiceAccount: ocv1.ServiceAccountReference{ + Name: clusterExtension.Name, + }, + } + + t.Log("By creating a new Deployment that can not be adopted") + newDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-operator", + Namespace: clusterExtension.Name, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test-operator"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "test-operator"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Command: []string{"sleep", "1000"}, + Image: "busybox", + ImagePullPolicy: corev1.PullAlways, + Name: "busybox", + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + RunAsUser: ptr.To(int64(1000)), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + }, + }, + }, + } + require.NoError(t, c.Create(context.Background(), newDeployment)) + + t.Log("It resolves the specified package with correct bundle path") + t.Log("By creating the ClusterExtension resource") + require.NoError(t, c.Create(context.Background(), clusterExtension)) + t.Log("By eventually reporting Progressing == True with Reason Retrying") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonRetrying, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) }, pollDuration, pollInterval) - t.Log("By eventually failing to install the package successfully due to insufficient ServiceAccount permissions") + t.Log("By eventually failing to install the package successfully due to no adoption support") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionFalse, cond.Status) - assert.Equal(ct, ocv1.ReasonFailed, cond.Reason) - assert.Equal(ct, "No bundle installed", cond.Message) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionFalse, cond.Status) + // TODO: We probably _should_ be testing the reason here, but helm and boxcutter applier have different reasons. + // Maybe we change helm to use "Absent" rather than "Failed" since the Progressing condition already captures + // the failure? + //require.Equal(ct, ocv1.ReasonFailed, cond.Reason) + require.Contains(ct, cond.Message, "No bundle installed") }, pollDuration, pollInterval) - t.Log("By fixing the ServiceAccount permissions") - require.NoError(t, createClusterRoleAndBindingForSA(context.Background(), name, sa, clusterExtension.Name)) + t.Log("By deleting the new Deployment") + require.NoError(t, c.Delete(context.Background(), newDeployment)) // NOTE: In order to ensure predictable results we need to ensure we have a single // known failure with a singular fix operation. Additionally, due to the exponential // backoff of this eventually check we MUST ensure we do not touch the ClusterExtension - // after creating and binding the needed permissions to the ServiceAccount. + // after deleting the Deployment. t.Log("By eventually installing the package successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - assert.NotEmpty(ct, clusterExtension.Status.Install) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotEmpty(ct, clusterExtension.Status.Install) }, pollDuration, pollInterval) t.Log("By eventually reporting Progressing == True with Reason Success") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - if assert.NotNil(ct, cond) { - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, pollDuration, pollInterval) } diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 354ef75f42..aa033a2f1e 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -2,19 +2,19 @@ package e2e import ( "context" + "fmt" "os" "testing" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/rest" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" ) var ( @@ -23,9 +23,8 @@ var ( ) const ( - testCatalogRefEnvVar = "CATALOG_IMG" - testCatalogName = "test-catalog" - latestImageTag = "latest" + testSummaryOutputEnvVar = "E2E_SUMMARY_OUTPUT" + latestImageTag = "latest" ) func TestMain(m *testing.M) { @@ -36,33 +35,19 @@ func TestMain(m *testing.M) { c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) utilruntime.Must(err) - os.Exit(m.Run()) -} - -// createTestCatalog will create a new catalog on the test cluster, provided -// the context, catalog name, and the image reference. It returns the created catalog -// or an error if any errors occurred while creating the catalog. -// Note that catalogd will automatically create the label: -// -// "olm.operatorframework.io/metadata.name": name -func createTestCatalog(ctx context.Context, name string, imageRef string) (*ocv1.ClusterCatalog, error) { - catalog := &ocv1.ClusterCatalog{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: ocv1.ClusterCatalogSpec{ - Source: ocv1.CatalogSource{ - Type: ocv1.SourceTypeImage, - Image: &ocv1.ImageSource{ - Ref: imageRef, - PollIntervalMinutes: ptr.To(1), - }, - }, - }, + res := m.Run() + path := os.Getenv(testSummaryOutputEnvVar) + if path == "" { + fmt.Printf("Note: E2E_SUMMARY_OUTPUT is unset; skipping summary generation") + } else { + err = utils.PrintSummary(path) + if err != nil { + // Fail the run if alerts are found + fmt.Printf("%v", err) + os.Exit(1) + } } - - err := c.Create(ctx, catalog) - return catalog, err + os.Exit(res) } // patchTestCatalog will patch the existing clusterCatalog on the test cluster, provided diff --git a/test/e2e/metrics_test.go b/test/e2e/metrics_test.go index 4a88c3dca7..e41827987d 100644 --- a/test/e2e/metrics_test.go +++ b/test/e2e/metrics_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/rand" - "github.com/operator-framework/operator-controller/test/utils" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" ) // TestOperatorControllerMetricsExportedEndpoint verifies that the metrics endpoint for the operator controller @@ -129,7 +129,7 @@ func (c *MetricsTestConfig) getServiceAccountToken(t *testing.T) string { func (c *MetricsTestConfig) createCurlMetricsPod(t *testing.T) { t.Logf("Creating curl pod (%s/%s) to validate the metrics endpoint", c.namespace, c.curlPodName) cmd := exec.Command(c.client, "run", c.curlPodName, - "--image=curlimages/curl", + "--image=quay.io/curl/curl:8.15.0", "--namespace", c.namespace, "--restart=Never", "--overrides", `{ @@ -137,7 +137,7 @@ func (c *MetricsTestConfig) createCurlMetricsPod(t *testing.T) { "terminationGradePeriodSeconds": 0, "containers": [{ "name": "curl", - "image": "curlimages/curl", + "image": "quay.io/curl/curl:8.15.0", "command": ["sh", "-c", "sleep 3600"], "securityContext": { "allowPrivilegeEscalation": false, diff --git a/test/e2e/network_policy_test.go b/test/e2e/network_policy_test.go new file mode 100644 index 0000000000..00143df416 --- /dev/null +++ b/test/e2e/network_policy_test.go @@ -0,0 +1,379 @@ +package e2e + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" +) + +const ( + minJustificationLength = 40 + catalogdManagerSelector = "control-plane=catalogd-controller-manager" + operatorManagerSelector = "control-plane=operator-controller-controller-manager" + catalogdMetricsPort = 7443 + catalogdWebhookPort = 9443 + catalogServerPort = 8443 + operatorControllerMetricsPort = 8443 +) + +type portWithJustification struct { + port []networkingv1.NetworkPolicyPort + justification string +} + +// ingressRule defines a k8s IngressRule, along with a justification. +type ingressRule struct { + ports []portWithJustification + from []networkingv1.NetworkPolicyPeer +} + +// egressRule defines a k8s egressRule, along with a justification. +type egressRule struct { + ports []portWithJustification + to []networkingv1.NetworkPolicyPeer +} + +// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy. +type allowedPolicyDefinition struct { + selector metav1.LabelSelector + policyTypes []networkingv1.PolicyType + ingressRule ingressRule + egressRule egressRule + denyAllIngressJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty + denyAllEgressJustification string // Justification if Egress is in PolicyTypes and EgressRules is empty +} + +var denyAllPolicySpec = allowedPolicyDefinition{ + selector: metav1.LabelSelector{}, // Empty selector, matches all pods + policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + // No IngressRules means deny all ingress if PolicyTypeIngress is present + // No EgressRules means deny all egress if PolicyTypeEgress is present + denyAllIngressJustification: "Denies all ingress traffic to pods selected by this policy by default, unless explicitly allowed by other policy rules, ensuring a baseline secure posture.", + denyAllEgressJustification: "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.", +} + +var prometheuSpec = allowedPolicyDefinition{ + selector: metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "prometheus"}}, + policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + ingressRule: ingressRule{ + ports: []portWithJustification{ + { + port: nil, + justification: "Allows access to the prometheus pod", + }, + }, + }, + egressRule: egressRule{ + ports: []portWithJustification{ + { + port: nil, + justification: "Allows prometheus to access other pods", + }, + }, + }, +} + +// Ref: https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?usp=sharing +var allowedNetworkPolicies = map[string]allowedPolicyDefinition{ + "catalogd-controller-manager": { + selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}}, + policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + ingressRule: ingressRule{ + ports: []portWithJustification{ + { + port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdMetricsPort}}}, + justification: "Allows Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health.", + }, + { + port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdWebhookPort}}}, + justification: "Permits Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources.", + }, + { + port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogServerPort}}}, + justification: "Enables clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery.", + }, + }, + }, + egressRule: egressRule{ + ports: []portWithJustification{ + { + port: nil, // Empty Ports means allow all egress + justification: "Permits catalogd to fetch catalog images from arbitrary container registries and communicate with the Kubernetes API server for its operational needs.", + }, + }, + }, + }, + "operator-controller-controller-manager": { + selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}}, + policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + ingressRule: ingressRule{ + ports: []portWithJustification{ + { + port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: operatorControllerMetricsPort}}}, + justification: "Allows Prometheus to scrape metrics from operator-controller, which is crucial for monitoring its activity, reconciliations, and overall health.", + }, + }, + }, + egressRule: egressRule{ + ports: []portWithJustification{ + { + port: nil, // Empty Ports means allow all egress + justification: "Enables operator-controller to pull bundle images from arbitrary image registries, connect to catalogd's HTTPS server for metadata, and interact with the Kubernetes API server.", + }, + }, + }, + }, +} + +func TestNetworkPolicyJustifications(t *testing.T) { + ctx := context.Background() + + // Validate justifications have min length in the allowedNetworkPolicies definition + for name, policyDef := range allowedNetworkPolicies { + for i, pwj := range policyDef.ingressRule.ports { + require.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength, + "Justification for ingress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification) + } + for i, pwj := range policyDef.egressRule.ports { // Corrected variable name from 'rule' to 'pwj' + require.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength, + "Justification for egress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification) + } + if policyDef.denyAllIngressJustification != "" { + require.GreaterOrEqualf(t, len(policyDef.denyAllIngressJustification), minJustificationLength, + "DenyAllIngressJustification for policy %q is too short: %q", name, policyDef.denyAllIngressJustification) + } + if policyDef.denyAllEgressJustification != "" { + require.GreaterOrEqualf(t, len(policyDef.denyAllEgressJustification), minJustificationLength, + "DenyAllEgressJustification for policy %q is too short: %q", name, policyDef.denyAllEgressJustification) + } + } + + clientForComponent := utils.FindK8sClient(t) + + operatorControllerNamespace := getComponentNamespace(t, clientForComponent, operatorManagerSelector) + catalogDNamespace := getComponentNamespace(t, clientForComponent, catalogdManagerSelector) + + policies := &networkingv1.NetworkPolicyList{} + err := c.List(ctx, policies, client.InNamespace(operatorControllerNamespace)) + require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", operatorControllerNamespace) + + clusterPolicies := policies.Items + + if operatorControllerNamespace != catalogDNamespace { + policies := &networkingv1.NetworkPolicyList{} + err := c.List(ctx, policies, client.InNamespace(catalogDNamespace)) + require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", catalogDNamespace) + clusterPolicies = append(clusterPolicies, policies.Items...) + + t.Log("Detected dual-namespace configuration, expecting two prefixed 'default-deny-all-traffic' policies.") + allowedNetworkPolicies["catalogd-default-deny-all-traffic"] = denyAllPolicySpec + allowedNetworkPolicies["operator-controller-default-deny-all-traffic"] = denyAllPolicySpec + } else { + t.Log("Detected single-namespace configuration, expecting one 'default-deny-all-traffic' policy.") + allowedNetworkPolicies["default-deny-all-traffic"] = denyAllPolicySpec + t.Log("Detected single-namespace configuration, expecting 'prometheus' policy.") + allowedNetworkPolicies["prometheus"] = prometheuSpec + } + + validatedRegistryPolicies := make(map[string]bool) + + for _, policy := range clusterPolicies { + t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policy.Name, "-", "_")), func(t *testing.T) { + expectedPolicy, found := allowedNetworkPolicies[policy.Name] + require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policy.Name, policy.Namespace) + validatedRegistryPolicies[policy.Name] = true + + // 1. Compare PodSelector + require.True(t, equality.Semantic.DeepEqual(expectedPolicy.selector, policy.Spec.PodSelector), + "PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policy.Name, expectedPolicy.selector, policy.Spec.PodSelector) + + // 2. Compare PolicyTypes + require.ElementsMatchf(t, expectedPolicy.policyTypes, policy.Spec.PolicyTypes, + "PolicyTypes mismatch for policy %q.", policy.Name) + + // 3. Validate Ingress Rules + hasIngressPolicyType := false + for _, pt := range policy.Spec.PolicyTypes { + if pt == networkingv1.PolicyTypeIngress { + hasIngressPolicyType = true + break + } + } + + if hasIngressPolicyType { + switch len(policy.Spec.Ingress) { + case 0: + validateDenyAllIngress(t, policy.Name, expectedPolicy) + case 1: + validateSingleIngressRule(t, policy.Name, policy.Spec.Ingress[0], expectedPolicy) + default: + require.Failf(t, "Policy %q in cluster has %d ingress rules. Allowed definition supports at most 1 explicit ingress rule.", policy.Name, len(policy.Spec.Ingress)) + } + } else { + validateNoIngress(t, policy.Name, policy, expectedPolicy) + } + + // 4. Validate Egress Rules + hasEgressPolicyType := false + for _, pt := range policy.Spec.PolicyTypes { + if pt == networkingv1.PolicyTypeEgress { + hasEgressPolicyType = true + break + } + } + + if hasEgressPolicyType { + switch len(policy.Spec.Egress) { + case 0: + validateDenyAllEgress(t, policy.Name, expectedPolicy) + case 1: + validateSingleEgressRule(t, policy.Name, policy.Spec.Egress[0], expectedPolicy) + default: + require.Failf(t, "Policy %q in cluster has %d egress rules. Allowed definition supports at most 1 explicit egress rule.", policy.Name, len(policy.Spec.Egress)) + } + } else { + validateNoEgress(t, policy, expectedPolicy) + } + }) + } + + // 5. Ensure all policies in the registry were found in the cluster + require.Len(t, validatedRegistryPolicies, len(allowedNetworkPolicies), + "Mismatch between number of expected policies in registry (%d) and number of policies found & validated in cluster (%d). Missing policies from registry: %v", len(allowedNetworkPolicies), len(validatedRegistryPolicies), missingPolicies(allowedNetworkPolicies, validatedRegistryPolicies)) +} + +func missingPolicies(expected map[string]allowedPolicyDefinition, actual map[string]bool) []string { + missing := []string{} + for k := range expected { + if !actual[k] { + missing = append(missing, k) + } + } + return missing +} + +// validateNoEgress confirms that a policy which does not have spec.PolicyType=Egress specified +// has no corresponding egress rules or expectations defined. +func validateNoEgress(t *testing.T, policy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) { + // Policy is NOT expected to affect Egress traffic (no Egress in PolicyTypes) + // Expected: Cluster has no egress rules; Registry has no DenyAllEgressJustification and empty EgressRule. + require.Emptyf(t, policy.Spec.Egress, + "Policy %q: Cluster does not have Egress PolicyType, but has Egress rules defined.", policy.Name) + require.Emptyf(t, expectedPolicy.denyAllEgressJustification, + "Policy %q: Cluster does not have Egress PolicyType. Registry's DenyAllEgressJustification is not empty.", policy.Name) + require.Emptyf(t, expectedPolicy.egressRule.ports, + "Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.Ports is not empty.", policy.Name) + require.Emptyf(t, expectedPolicy.egressRule.to, + "Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.To is not empty.", policy.Name) +} + +// validateDenyAllEgress confirms that a policy with Egress PolicyType but no explicit rules +// correctly corresponds to a "deny all" expectation. +func validateDenyAllEgress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) { + // Cluster: PolicyType Egress is present, but no explicit egress rules -> Deny All Egress by this policy. + // Expected: DenyAllEgressJustification is set; EgressRule.Ports and .To are empty. + require.NotEmptyf(t, expectedPolicy.denyAllEgressJustification, + "Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's DenyAllEgressJustification is empty.", policyName) + require.Emptyf(t, expectedPolicy.egressRule.ports, + "Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.Ports is not empty.", policyName) + require.Emptyf(t, expectedPolicy.egressRule.to, + "Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.To is not empty.", policyName) +} + +// validateSingleEgressRule validates a policy that has exactly one explicit egress rule, +// distinguishing between "allow-all" and more specific rules. +func validateSingleEgressRule(t *testing.T, policyName string, clusterEgressRule networkingv1.NetworkPolicyEgressRule, expectedPolicy allowedPolicyDefinition) { + // Cluster: PolicyType Egress is present, and there's one explicit egress rule. + // Expected: DenyAllEgressJustification is empty; EgressRule matches the cluster's rule. + expectedEgressRule := expectedPolicy.egressRule + + require.Emptyf(t, expectedPolicy.denyAllEgressJustification, + "Policy %q: Cluster has a specific Egress rule. Registry's DenyAllEgressJustification should be empty.", policyName) + + isClusterRuleAllowAllPorts := len(clusterEgressRule.Ports) == 0 + isClusterRuleAllowAllPeers := len(clusterEgressRule.To) == 0 + + if isClusterRuleAllowAllPorts && isClusterRuleAllowAllPeers { // Handles egress: [{}] - allow all ports to all peers + require.Lenf(t, expectedEgressRule.ports, 1, + "Policy %q (allow-all egress): Expected EgressRule.Ports to have 1 justification entry, got %d", policyName, len(expectedEgressRule.ports)) + if len(expectedEgressRule.ports) == 1 { // Guard against panic + require.Nilf(t, expectedEgressRule.ports[0].port, + "Policy %q (allow-all egress): Expected EgressRule.Ports[0].Port to be nil, got %+v", policyName, expectedEgressRule.ports[0].port) + } + require.Conditionf(t, func() bool { return len(expectedEgressRule.to) == 0 }, + "Policy %q (allow-all egress): Expected EgressRule.To to be empty for allow-all peers, got %+v", policyName, expectedEgressRule.to) + } else { + // Specific egress rule (not the simple allow-all ports and allow-all peers) + require.True(t, equality.Semantic.DeepEqual(expectedEgressRule.to, clusterEgressRule.To), + "Policy %q, Egress Rule: 'To' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedEgressRule.to, clusterEgressRule.To) + + var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort + for _, pwj := range expectedEgressRule.ports { + allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...) + } + require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterEgressRule.Ports, + "Policy %q, Egress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterEgressRule.Ports) + } +} + +// validateNoIngress confirms that a policy which does not have the Ingress PolicyType +// has no corresponding ingress rules or expectations defined. +func validateNoIngress(t *testing.T, policyName string, clusterPolicy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) { + // Policy is NOT expected to affect Ingress traffic (no Ingress in PolicyTypes) + // Expected: Cluster has no ingress rules; Registry has no DenyAllIngressJustification and empty IngressRule. + require.Emptyf(t, clusterPolicy.Spec.Ingress, + "Policy %q: Cluster does not have Ingress PolicyType, but has Ingress rules defined.", policyName) + require.Emptyf(t, expectedPolicy.denyAllIngressJustification, + "Policy %q: Cluster does not have Ingress PolicyType. Registry's DenyAllIngressJustification is not empty.", policyName) + require.Emptyf(t, expectedPolicy.ingressRule.ports, + "Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.Ports is not empty.", policyName) + require.Emptyf(t, expectedPolicy.ingressRule.from, + "Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.From is not empty.", policyName) +} + +// validateDenyAllIngress confirms that a policy with Ingress PolicyType but no explicit rules +// correctly corresponds to a "deny all" expectation. +func validateDenyAllIngress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) { + // Cluster: PolicyType Ingress is present, but no explicit ingress rules -> Deny All Ingress by this policy. + // Expected: DenyAllIngressJustification is set; IngressRule.Ports and .From are empty. + require.NotEmptyf(t, expectedPolicy.denyAllIngressJustification, + "Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's DenyAllIngressJustification is empty.", policyName) + require.Emptyf(t, expectedPolicy.ingressRule.ports, + "Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.Ports is not empty.", policyName) + require.Emptyf(t, expectedPolicy.ingressRule.from, + "Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.From is not empty.", policyName) +} + +// validateSingleIngressRule validates a policy that has exactly one explicit ingress rule. +func validateSingleIngressRule(t *testing.T, policyName string, clusterIngressRule networkingv1.NetworkPolicyIngressRule, expectedPolicy allowedPolicyDefinition) { + // Cluster: PolicyType Ingress is present, and there's one explicit ingress rule. + // Expected: DenyAllIngressJustification is empty; IngressRule matches the cluster's rule. + expectedIngressRule := expectedPolicy.ingressRule + + require.Emptyf(t, expectedPolicy.denyAllIngressJustification, + "Policy %q: Cluster has a specific Ingress rule. Registry's DenyAllIngressJustification should be empty.", policyName) + + // Compare 'From' + require.True(t, equality.Semantic.DeepEqual(expectedIngressRule.from, clusterIngressRule.From), + "Policy %q, Ingress Rule: 'From' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedIngressRule.from, clusterIngressRule.From) + + // Compare 'Ports' by aggregating the ports from our justified structure + var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort + for _, pwj := range expectedIngressRule.ports { + allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...) + } + require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterIngressRule.Ports, + "Policy %q, Ingress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterIngressRule.Ports) +} diff --git a/test/e2e/single_namespace_support_test.go b/test/e2e/single_namespace_support_test.go new file mode 100644 index 0000000000..86ec782202 --- /dev/null +++ b/test/e2e/single_namespace_support_test.go @@ -0,0 +1,348 @@ +package e2e + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" +) + +func TestClusterExtensionSingleNamespaceSupport(t *testing.T) { + t.Log("Test support for cluster extension config") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating install namespace, watch namespace and necessary rbac resources") + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-namespace-operator", + }, + } + require.NoError(t, c.Create(t.Context(), &namespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &namespace)) + }) + + watchNamespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-namespace-operator-target", + }, + } + require.NoError(t, c.Create(t.Context(), &watchNamespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &watchNamespace)) + }) + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-namespace-operator-installer", + Namespace: namespace.GetName(), + }, + } + require.NoError(t, c.Create(t.Context(), &serviceAccount)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &serviceAccount)) + }) + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-namespace-operator-installer", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: corev1.GroupName, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + require.NoError(t, c.Create(t.Context(), clusterRoleBinding)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) + }) + + t.Log("By creating the test-catalog ClusterCatalog") + extensionCatalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("CLUSTER_REGISTRY_HOST")), + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + require.NoError(t, c.Create(t.Context(), extensionCatalog)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), extensionCatalog)) + }) + + t.Log("By waiting for the catalog to serve its metadata") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog)) + cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By attempting to install the single-namespace-operator ClusterExtension without any configuration") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-namespace-operator-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "single-namespace-operator", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, + }, + }, + Namespace: namespace.GetName(), + ServiceAccount: ocv1.ServiceAccountReference{ + Name: serviceAccount.GetName(), + }, + }, + } + require.NoError(t, c.Create(t.Context(), clusterExtension)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + }) + + t.Log("By waiting for single-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, "required field \"watchNamespace\" is missing") + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(fmt.Sprintf(`{"watchNamespace": "%s"}`, watchNamespace.GetName())), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for single-namespace-operator extension to be installed successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotNil(ct, clusterExtension.Status.Install) + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + }, pollDuration, pollInterval) + + t.Log("By ensuring the single-namespace-operator deployment is correctly configured to watch the watch namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + deployment := &appsv1.Deployment{} + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "single-namespace-operator"}, deployment)) + require.NotNil(ct, deployment.Spec.Template.GetAnnotations()) + require.Equal(ct, watchNamespace.GetName(), deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) + }, pollDuration, pollInterval) +} + +func TestClusterExtensionOwnNamespaceSupport(t *testing.T) { + t.Log("Test support for cluster extension with OwnNamespace install mode support") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating install namespace, watch namespace and necessary rbac resources") + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator", + }, + } + require.NoError(t, c.Create(t.Context(), &namespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &namespace)) + }) + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-installer", + Namespace: namespace.GetName(), + }, + } + require.NoError(t, c.Create(t.Context(), &serviceAccount)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &serviceAccount)) + }) + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-installer", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: corev1.GroupName, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + require.NoError(t, c.Create(t.Context(), clusterRoleBinding)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) + }) + + t.Log("By creating the test-catalog ClusterCatalog") + extensionCatalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("CLUSTER_REGISTRY_HOST")), + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + require.NoError(t, c.Create(t.Context(), extensionCatalog)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), extensionCatalog)) + }) + + t.Log("By waiting for the catalog to serve its metadata") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog)) + cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By attempting to install the own-namespace-operator ClusterExtension without any configuration") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "own-namespace-operator", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, + }, + }, + Namespace: namespace.GetName(), + ServiceAccount: ocv1.ServiceAccountReference{ + Name: serviceAccount.GetName(), + }, + }, + } + require.NoError(t, c.Create(t.Context(), clusterExtension)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + }) + + t.Log("By waiting for own-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, "required field \"watchNamespace\" is missing") + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace other than the install namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "some-namespace"}`), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for own-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, fmt.Sprintf("invalid 'watchNamespace' \"some-namespace\": must be install namespace (%s)", clusterExtension.Spec.Namespace)) + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace = install namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(fmt.Sprintf(`{"watchNamespace": "%s"}`, clusterExtension.Spec.Namespace)), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for own-namespace-operator extension to be installed successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotNil(ct, clusterExtension.Status.Install) + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + }, pollDuration, pollInterval) + + t.Log("By ensuring the own-namespace-operator deployment is correctly configured to watch the watch namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + deployment := &appsv1.Deployment{} + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "own-namespace-operator"}, deployment)) + require.NotNil(ct, deployment.Spec.Template.GetAnnotations()) + require.Equal(ct, clusterExtension.Spec.Namespace, deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) + }, pollDuration, pollInterval) +} diff --git a/test/e2e/webhook_support_test.go b/test/e2e/webhook_support_test.go new file mode 100644 index 0000000000..0809efb541 --- /dev/null +++ b/test/e2e/webhook_support_test.go @@ -0,0 +1,240 @@ +package e2e + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" +) + +var dynamicClient dynamic.Interface + +func TestNoop(t *testing.T) { + t.Log("Running experimental-e2e tests") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) +} + +func TestWebhookSupport(t *testing.T) { + t.Log("Test support for bundles with webhooks") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + if dynamicClient == nil { + var err error + dynamicClient, err = dynamic.NewForConfig(cfg) + require.NoError(t, err) + } + + t.Log("By creating install namespace, and necessary rbac resources") + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator", + }, + } + require.NoError(t, c.Create(t.Context(), &namespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &namespace)) + }) + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-installer", + Namespace: namespace.GetName(), + }, + } + require.NoError(t, c.Create(t.Context(), &serviceAccount)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &serviceAccount)) + }) + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-installer", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: corev1.GroupName, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + require.NoError(t, c.Create(t.Context(), clusterRoleBinding)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) + }) + + t.Log("By creating the webhook-operator ClusterCatalog") + extensionCatalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("CLUSTER_REGISTRY_HOST")), + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + require.NoError(t, c.Create(t.Context(), extensionCatalog)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), extensionCatalog)) + }) + + t.Log("By waiting for the catalog to serve its metadata") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog)) + cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By installing the webhook-operator ClusterExtension") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webhook-operator-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "webhook-operator", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, + }, + }, + Namespace: namespace.GetName(), + ServiceAccount: ocv1.ServiceAccountReference{ + Name: serviceAccount.GetName(), + }, + }, + } + require.NoError(t, c.Create(t.Context(), clusterExtension)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + }) + + t.Log("By waiting for webhook-operator extension to be installed successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotNil(ct, clusterExtension.Status.Install) + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + }, pollDuration, pollInterval) + + t.Log("By waiting for webhook-operator deployment to be available") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + deployment := &appsv1.Deployment{} + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "webhook-operator-controller-manager"}, deployment)) + available := false + for _, cond := range deployment.Status.Conditions { + if cond.Type == appsv1.DeploymentAvailable { + available = cond.Status == corev1.ConditionTrue + } + } + require.True(ct, available) + }, pollDuration, pollInterval) + + v1Gvr := schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v1", + Resource: "webhooktests", + } + v1Client := dynamicClient.Resource(v1Gvr).Namespace(namespace.GetName()) + + t.Log("By eventually seeing that invalid CR creation is rejected by the validating webhook") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + obj := getWebhookOperatorResource("invalid-test-cr", namespace.GetName(), false) + _, err := v1Client.Create(t.Context(), obj, metav1.CreateOptions{}) + require.Error(ct, err) + require.Contains(ct, err.Error(), "Invalid value: false: Spec.Valid must be true") + }, pollDuration, pollInterval) + + var ( + res *unstructured.Unstructured + err error + obj = getWebhookOperatorResource("valid-test-cr", namespace.GetName(), true) + ) + + t.Log("By eventually creating a valid CR") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + res, err = v1Client.Create(t.Context(), obj, metav1.CreateOptions{}) + require.NoError(ct, err) + }, pollDuration, pollInterval) + t.Cleanup(func() { + require.NoError(t, v1Client.Delete(context.Background(), obj.GetName(), metav1.DeleteOptions{})) + }) + + require.Equal(t, map[string]interface{}{ + "valid": true, + "mutate": true, + }, res.Object["spec"]) + + t.Log("By checking a valid CR is converted to v2 by the conversion webhook") + v2Gvr := schema.GroupVersionResource{ + Group: "webhook.operators.coreos.io", + Version: "v2", + Resource: "webhooktests", + } + v2Client := dynamicClient.Resource(v2Gvr).Namespace(namespace.GetName()) + + t.Log("By eventually getting the valid CR with a v2 client") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + res, err = v2Client.Get(t.Context(), obj.GetName(), metav1.GetOptions{}) + require.NoError(ct, err) + }, pollDuration, pollInterval) + + t.Log("and verifying that the CR is correctly converted") + require.Equal(t, map[string]interface{}{ + "conversion": map[string]interface{}{ + "valid": true, + "mutate": true, + }, + }, res.Object["spec"]) +} + +func getWebhookOperatorResource(name string, namespace string, valid bool) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "webhook.operators.coreos.io/v1", + "kind": "webhooktests", + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "valid": valid, + }, + }, + } +} diff --git a/test/experimental-e2e/boxcutter_support_test.go b/test/experimental-e2e/boxcutter_support_test.go new file mode 100644 index 0000000000..b09d19ec89 --- /dev/null +++ b/test/experimental-e2e/boxcutter_support_test.go @@ -0,0 +1,70 @@ +package experimental_e2e + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" + . "github.com/operator-framework/operator-controller/test/helpers" +) + +func TestClusterExtensionVersionUpdate(t *testing.T) { + t.Log("When a cluster extension is installed from a catalog") + t.Log("When resolving upgrade edges") + + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating an ClusterExtension at a specified version") + clusterExtension.Spec = ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test", + Version: "1.0.0", + }, + }, + Namespace: ns.Name, + ServiceAccount: ocv1.ServiceAccountReference{ + Name: sa.Name, + }, + } + require.NoError(t, c.Create(context.Background(), clusterExtension)) + t.Log("By eventually reporting a successful resolution") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("It allows to upgrade the ClusterExtension to a non-successor version") + t.Log("By forcing update of ClusterExtension resource to a non-successor version") + // 1.2.0 does not replace/skip/skipRange 1.0.0. + clusterExtension.Spec.Source.Catalog.Version = "1.2.0" + clusterExtension.Spec.Source.Catalog.UpgradeConstraintPolicy = ocv1.UpgradeConstraintPolicySelfCertified + require.NoError(t, c.Update(context.Background(), clusterExtension)) + t.Log("By eventually reporting a satisfiable resolution") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) + t.Log("We should have two ClusterExtensionRevision resources") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + cerList := &ocv1.ClusterExtensionRevisionList{} + require.NoError(ct, c.List(context.Background(), cerList)) + require.Len(ct, cerList.Items, 2) + }, pollDuration, pollInterval) +} diff --git a/test/experimental-e2e/experimental_test.go b/test/experimental-e2e/experimental_test.go new file mode 100644 index 0000000000..de329b588e --- /dev/null +++ b/test/experimental-e2e/experimental_test.go @@ -0,0 +1,43 @@ +package experimental_e2e + +import ( + "os" + "testing" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" +) + +const ( + artifactName = "operator-controller-experimental-e2e" + pollDuration = time.Minute + pollInterval = time.Second +) + +var ( + cfg *rest.Config + c client.Client +) + +func TestMain(m *testing.M) { + cfg = ctrl.GetConfigOrDie() + + var err error + utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) + c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + utilruntime.Must(err) + + os.Exit(m.Run()) +} + +func TestNoop(t *testing.T) { + t.Log("Running experimental-e2e tests") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) +} diff --git a/test/extension-developer-e2e/extension_developer_test.go b/test/extension-developer-e2e/extension_developer_test.go index c493887b0c..a71f1f83d6 100644 --- a/test/extension-developer-e2e/extension_developer_test.go +++ b/test/extension-developer-e2e/extension_developer_test.go @@ -32,6 +32,9 @@ func TestExtensionDeveloper(t *testing.T) { require.NoError(t, corev1.AddToScheme(scheme)) require.NoError(t, rbacv1.AddToScheme(scheme)) + require.NotEmpty(t, os.Getenv("CATALOG_IMG"), "environment variable CATALOG_IMG must be set") + require.NotEmpty(t, os.Getenv("REG_PKG_NAME"), "environment variable REG_PKG_NAME must be set") + c, err := client.New(cfg, client.Options{Scheme: scheme}) require.NoError(t, err) @@ -195,18 +198,16 @@ func TestExtensionDeveloper(t *testing.T) { } require.NoError(t, c.Create(ctx, crb)) - t.Logf("When creating an ClusterExtension that references a package with a %q bundle type", clusterExtension.ObjectMeta.Name) + t.Logf("When creating an ClusterExtension that references a package with a %q bundle type", clusterExtension.Name) require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("It should have a status condition type of Installed with a status of True and a reason of Success") require.EventuallyWithT(t, func(ct *assert.CollectT) { ext := &ocv1.ClusterExtension{} - assert.NoError(ct, c.Get(context.Background(), client.ObjectKeyFromObject(clusterExtension), ext)) + require.NoError(ct, c.Get(context.Background(), client.ObjectKeyFromObject(clusterExtension), ext)) cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) - if !assert.NotNil(ct, cond) { - return - } - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, 2*time.Minute, time.Second) require.NoError(t, c.Delete(context.Background(), catalog)) require.NoError(t, c.Delete(context.Background(), clusterExtension)) diff --git a/test/extension-developer-e2e/setup.sh b/test/extension-developer-e2e/setup.sh index 889080ad6c..293341b33b 100755 --- a/test/extension-developer-e2e/setup.sh +++ b/test/extension-developer-e2e/setup.sh @@ -11,27 +11,26 @@ following bundle formats: This script will ensure that all images built are loaded onto a KinD cluster with the name specified in the arguments. The following environment variables are required for configuring this script: -- \$CATALOG_IMG - the tag for the catalog image that contains the registry+v1 bundle. +- \$E2E_TEST_CATALOG_V1 - the tag for the catalog image that contains the registry+v1 bundle. - \$REG_PKG_NAME - the name of the package for the extension that uses the registry+v1 bundle format. -- \$LOCAL_REGISTRY_HOST - hostname:port of the local docker-registry setup.sh also takes 5 arguments. Usage: - setup.sh [OPERATOR_SDK] [CONTAINER_RUNTIME] [KUSTOMIZE] [KIND] [KIND_CLUSTER_NAME] [NAMESPACE] + setup.sh [OPERATOR_SDK] [CONTAINER_RUNTIME] [KUSTOMIZE] [LOCAL_REGISTRY_HOST] [CLUSTER_REGISTRY_HOST] " ######################################## # Input validation ######################################## -if [[ "$#" -ne 6 ]]; then +if [[ "$#" -ne 5 ]]; then echo "Illegal number of arguments passed" echo "${help}" exit 1 fi -if [[ -z "${CATALOG_IMG}" ]]; then - echo "\$CATALOG_IMG is required to be set" +if [[ -z "${E2E_TEST_CATALOG_V1}" ]]; then + echo "\$E2E_TEST_CATALOG_V1 is required to be set" echo "${help}" exit 1 fi @@ -42,12 +41,6 @@ if [[ -z "${REG_PKG_NAME}" ]]; then exit 1 fi -if [[ -z "${LOCAL_REGISTRY_HOST}" ]]; then - echo "\$LOCAL_REGISTRY_HOST is required to be set" - echo "${help}" - exit 1 -fi - ######################################## # Setup temp dir and local variables ######################################## @@ -64,15 +57,25 @@ mkdir -p "${REG_DIR}" operator_sdk=$1 container_tool=$2 kustomize=$3 -kind=$4 -kcluster_name=$5 -namespace=$6 +# The path we use to push the image from _outside_ the cluster +local_registry_host=$4 +# The path we use _inside_ the cluster +cluster_registry_host=$5 + +tls_flag="" +if [[ "$container_tool" == "podman" ]]; then + echo "Using podman container runtime; adding tls disable flag" + tls_flag="--tls-verify=false" +fi + +catalog_push_tag="${local_registry_host}/${E2E_TEST_CATALOG_V1}" +reg_pkg_name="${REG_PKG_NAME}" reg_img="${DOMAIN}/registry:v0.0.1" -reg_bundle_img="${LOCAL_REGISTRY_HOST}/bundles/registry-v1/registry-bundle:v0.0.1" +reg_bundle_path="bundles/registry-v1/registry-bundle:v0.0.1" -catalog_img="${CATALOG_IMG}" -reg_pkg_name="${REG_PKG_NAME}" +reg_bundle_img="${cluster_registry_host}/${reg_bundle_path}" +reg_bundle_push_tag="${local_registry_host}/${reg_bundle_path}" ######################################## # Create the registry+v1 based extension @@ -84,7 +87,7 @@ reg_pkg_name="${REG_PKG_NAME}" # NOTE: This is a rough edge that users will experience # The Makefile in the project scaffolded by operator-sdk uses an SDK binary -# in the path path if it is present. Override via `export` to ensure we use +# in the path if it is present. Override via `export` to ensure we use # the same version that we scaffolded with. # NOTE: this is a rough edge that users will experience @@ -102,7 +105,8 @@ reg_pkg_name="${REG_PKG_NAME}" make docker-build IMG="${reg_img}" && \ sed -i -e 's/$(OPERATOR_SDK) generate kustomize manifests -q/$(OPERATOR_SDK) generate kustomize manifests -q --interactive=false/g' Makefile && \ make bundle IMG="${reg_img}" VERSION=0.0.1 && \ - make bundle-build BUNDLE_IMG="${reg_bundle_img}" + make bundle-build BUNDLE_IMG="${reg_bundle_push_tag}" + ${container_tool} push ${reg_bundle_push_tag} ${tls_flag} ) ############################### @@ -149,107 +153,5 @@ cat < "${TMP_ROOT}"/catalog/index.yaml } EOF -# Add a .indexignore to make catalogd ignore -# reading the symlinked ..* files that are created when -# mounting a ConfigMap -cat < "${TMP_ROOT}"/catalog/.indexignore -..* -EOF - -kubectl create configmap -n "${namespace}" --from-file="${TMP_ROOT}"/catalog.Dockerfile extension-dev-e2e.dockerfile -kubectl create configmap -n "${namespace}" --from-file="${TMP_ROOT}"/catalog extension-dev-e2e.build-contents - -kubectl apply -f - << EOF -apiVersion: batch/v1 -kind: Job -metadata: - name: kaniko - namespace: "${namespace}" -spec: - template: - spec: - containers: - - name: kaniko - image: gcr.io/kaniko-project/executor:latest - args: ["--dockerfile=/workspace/catalog.Dockerfile", - "--context=/workspace/", - "--destination=${catalog_img}", - "--skip-tls-verify"] - volumeMounts: - - name: dockerfile - mountPath: /workspace/ - - name: build-contents - mountPath: /workspace/catalog/ - restartPolicy: Never - volumes: - - name: dockerfile - configMap: - name: extension-dev-e2e.dockerfile - items: - - key: catalog.Dockerfile - path: catalog.Dockerfile - - name: build-contents - configMap: - name: extension-dev-e2e.build-contents -EOF - -kubectl wait --for=condition=Complete -n "${namespace}" jobs/kaniko --timeout=60s - -# Make sure all files are removable. This is necessary because -# the Makefiles generated by the Operator-SDK have targets -# that install binaries under the bin/ directory. Those binaries -# don't have write permissions so they can't be removed unless -# we ensure they have the write permissions -chmod -R +w "${REG_DIR}/bin" - -# Load the bundle image into the docker-registry - -kubectl create configmap -n "${namespace}" --from-file="${REG_DIR}/bundle.Dockerfile" operator-controller-e2e-${reg_pkg_name}.root - -tgz="${REG_DIR}/manifests.tgz" -tar czf "${tgz}" -C "${REG_DIR}" bundle -kubectl create configmap -n "${namespace}" --from-file="${tgz}" operator-controller-${reg_pkg_name}.manifests - -kubectl apply -f - << EOF -apiVersion: batch/v1 -kind: Job -metadata: - name: "kaniko-${reg_pkg_name}" - namespace: "${namespace}" -spec: - template: - spec: - initContainers: - - name: copy-manifests - image: busybox - command: ['sh', '-c', 'cp /manifests-data/* /manifests'] - volumeMounts: - - name: manifests - mountPath: /manifests - - name: manifests-data - mountPath: /manifests-data - containers: - - name: kaniko - image: gcr.io/kaniko-project/executor:latest - args: ["--dockerfile=/workspace/bundle.Dockerfile", - "--context=tar:///workspace/manifests/manifests.tgz", - "--destination=${reg_bundle_img}", - "--skip-tls-verify"] - volumeMounts: - - name: dockerfile - mountPath: /workspace/ - - name: manifests - mountPath: /workspace/manifests/ - restartPolicy: Never - volumes: - - name: dockerfile - configMap: - name: operator-controller-e2e-${reg_pkg_name}.root - - name: manifests - emptyDir: {} - - name: manifests-data - configMap: - name: operator-controller-${reg_pkg_name}.manifests -EOF - -kubectl wait --for=condition=Complete -n "${namespace}" jobs/kaniko-${reg_pkg_name} --timeout=60s +${container_tool} build -f "${TMP_ROOT}/catalog.Dockerfile" -t "${catalog_push_tag}" "${TMP_ROOT}/" +${container_tool} push ${catalog_push_tag} ${tls_flag} diff --git a/test/helpers/helpers.go b/test/helpers/helpers.go new file mode 100644 index 0000000000..49ebeaab6a --- /dev/null +++ b/test/helpers/helpers.go @@ -0,0 +1,383 @@ +package utils + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" +) + +var ( + cfg *rest.Config + c client.Client +) + +const ( + pollDuration = time.Minute + pollInterval = time.Second + testCatalogName = "test-catalog" + testCatalogRefEnvVar = "CATALOG_IMG" +) + +func CreateNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + err := c.Create(ctx, ns) + if err != nil { + return nil, err + } + return ns, nil +} + +func CreateServiceAccount(ctx context.Context, name types.NamespacedName, clusterExtensionName string) (*corev1.ServiceAccount, error) { + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + } + err := c.Create(ctx, sa) + if err != nil { + return nil, err + } + + return sa, CreateClusterRoleAndBindingForSA(ctx, name.Name, sa, clusterExtensionName) +} + +func CreateClusterRoleAndBindingForSA(ctx context.Context, name string, sa *corev1.ServiceAccount, clusterExtensionName string) error { + cr := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{ + "olm.operatorframework.io", + }, + Resources: []string{ + "clusterextensions/finalizers", + }, + Verbs: []string{ + "update", + }, + ResourceNames: []string{clusterExtensionName}, + }, + { + APIGroups: []string{ + "", + }, + Resources: []string{ + "configmaps", + "secrets", // for helm + "services", + "serviceaccounts", + }, + Verbs: []string{ + "create", + "update", + "delete", + "patch", + "get", + "list", + "watch", + }, + }, + { + APIGroups: []string{ + "apiextensions.k8s.io", + }, + Resources: []string{ + "customresourcedefinitions", + }, + Verbs: []string{ + "create", + "update", + "delete", + "patch", + "get", + "list", + "watch", + }, + }, + { + APIGroups: []string{ + "apps", + }, + Resources: []string{ + "deployments", + }, + Verbs: []string{ + "create", + "update", + "delete", + "patch", + "get", + "list", + "watch", + }, + }, + { + APIGroups: []string{ + "rbac.authorization.k8s.io", + }, + Resources: []string{ + "clusterroles", + "roles", + "clusterrolebindings", + "rolebindings", + }, + Verbs: []string{ + "create", + "update", + "delete", + "patch", + "get", + "list", + "watch", + "bind", + "escalate", + }, + }, + { + APIGroups: []string{ + "networking.k8s.io", + }, + Resources: []string{ + "networkpolicies", + }, + Verbs: []string{ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete", + }, + }, + }, + } + err := c.Create(ctx, cr) + if err != nil { + return err + } + crb := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: name, + }, + } + err = c.Create(ctx, crb) + if err != nil { + return err + } + + return nil +} + +func TestInit(t *testing.T) (*ocv1.ClusterExtension, *ocv1.ClusterCatalog, *corev1.ServiceAccount, *corev1.Namespace) { + ce, cc := TestInitClusterExtensionClusterCatalog(t) + sa, ns := TestInitServiceAccountNamespace(t, ce.Name) + return ce, cc, sa, ns +} + +func TestInitClusterExtensionClusterCatalog(t *testing.T) (*ocv1.ClusterExtension, *ocv1.ClusterCatalog) { + ceName := fmt.Sprintf("clusterextension-%s", rand.String(8)) + + ce := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: ceName, + }, + } + + cc, err := CreateTestCatalog(context.Background(), testCatalogName, os.Getenv(testCatalogRefEnvVar)) + require.NoError(t, err) + + ValidateCatalogUnpack(t) + + return ce, cc +} + +func TestInitServiceAccountNamespace(t *testing.T, clusterExtensionName string) (*corev1.ServiceAccount, *corev1.Namespace) { + var err error + + ns, err := CreateNamespace(context.Background(), clusterExtensionName) + require.NoError(t, err) + + name := types.NamespacedName{ + Name: clusterExtensionName, + Namespace: ns.GetName(), + } + + sa, err := CreateServiceAccount(context.Background(), name, clusterExtensionName) + require.NoError(t, err) + + return sa, ns +} + +func ValidateCatalogUnpack(t *testing.T) { + catalog := &ocv1.ClusterCatalog{} + t.Log("Ensuring ClusterCatalog has Status.Condition of Progressing with a status == True and reason == Succeeded") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + err := c.Get(context.Background(), types.NamespacedName{Name: testCatalogName}, catalog) + require.NoError(ct, err) + cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("Checking that catalog has the expected metadata label") + require.NotNil(t, catalog.Labels) + require.Contains(t, catalog.Labels, "olm.operatorframework.io/metadata.name") + require.Equal(t, testCatalogName, catalog.Labels["olm.operatorframework.io/metadata.name"]) + + t.Log("Ensuring ClusterCatalog has Status.Condition of Type = Serving with status == True") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + err := c.Get(context.Background(), types.NamespacedName{Name: testCatalogName}, catalog) + require.NoError(ct, err) + cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeServing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) +} + +func EnsureNoExtensionResources(t *testing.T, clusterExtensionName string) { + ls := labels.Set{"olm.operatorframework.io/owner-name": clusterExtensionName} + + // CRDs may take an extra long time to be deleted, and may run into the following error: + // Condition=Terminating Status=True Reason=InstanceDeletionFailed Message="could not list instances: storage is (re)initializing" + t.Logf("By waiting for CustomResourceDefinitions of %q to be deleted", clusterExtensionName) + require.EventuallyWithT(t, func(ct *assert.CollectT) { + list := &apiextensionsv1.CustomResourceDefinitionList{} + err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) + require.NoError(ct, err) + require.Empty(ct, list.Items) + }, 5*pollDuration, pollInterval) + + t.Logf("By waiting for ClusterRoleBindings of %q to be deleted", clusterExtensionName) + require.EventuallyWithT(t, func(ct *assert.CollectT) { + list := &rbacv1.ClusterRoleBindingList{} + err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) + require.NoError(ct, err) + require.Empty(ct, list.Items) + }, 2*pollDuration, pollInterval) + + t.Logf("By waiting for ClusterRoles of %q to be deleted", clusterExtensionName) + require.EventuallyWithT(t, func(ct *assert.CollectT) { + list := &rbacv1.ClusterRoleList{} + err := c.List(context.Background(), list, client.MatchingLabelsSelector{Selector: ls.AsSelector()}) + require.NoError(ct, err) + require.Empty(ct, list.Items) + }, 2*pollDuration, pollInterval) +} + +func TestCleanup(t *testing.T, cat *ocv1.ClusterCatalog, clusterExtension *ocv1.ClusterExtension, sa *corev1.ServiceAccount, ns *corev1.Namespace) { + if cat != nil { + t.Logf("By deleting ClusterCatalog %q", cat.Name) + require.NoError(t, c.Delete(context.Background(), cat)) + require.Eventually(t, func() bool { + err := c.Get(context.Background(), types.NamespacedName{Name: cat.Name}, &ocv1.ClusterCatalog{}) + return errors.IsNotFound(err) + }, pollDuration, pollInterval) + } + + if clusterExtension != nil { + t.Logf("By deleting ClusterExtension %q", clusterExtension.Name) + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + require.Eventually(t, func() bool { + err := c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, &ocv1.ClusterExtension{}) + return errors.IsNotFound(err) + }, pollDuration, pollInterval) + EnsureNoExtensionResources(t, clusterExtension.Name) + } + + if sa != nil { + t.Logf("By deleting ServiceAccount %q", sa.Name) + require.NoError(t, c.Delete(context.Background(), sa)) + require.Eventually(t, func() bool { + err := c.Get(context.Background(), types.NamespacedName{Name: sa.Name, Namespace: sa.Namespace}, &corev1.ServiceAccount{}) + return errors.IsNotFound(err) + }, pollDuration, pollInterval) + } + + if ns != nil { + t.Logf("By deleting Namespace %q", ns.Name) + require.NoError(t, c.Delete(context.Background(), ns)) + require.Eventually(t, func() bool { + err := c.Get(context.Background(), types.NamespacedName{Name: ns.Name}, &corev1.Namespace{}) + return errors.IsNotFound(err) + }, pollDuration, pollInterval) + } +} + +// CreateTestCatalog will create a new catalog on the test cluster, provided +// the context, catalog name, and the image reference. It returns the created catalog +// or an error if any errors occurred while creating the catalog. +// Note that catalogd will automatically create the label: +// +// "olm.operatorframework.io/metadata.name": name +func CreateTestCatalog(ctx context.Context, name string, imageRef string) (*ocv1.ClusterCatalog, error) { + catalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: imageRef, + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + + err := c.Create(ctx, catalog) + return catalog, err +} + +func init() { + cfg = ctrl.GetConfigOrDie() + + var err error + utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) + c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + utilruntime.Must(err) +} diff --git a/test/regression/convert/convert_test.go b/test/regression/convert/convert_test.go new file mode 100644 index 0000000000..1e70c08cf0 --- /dev/null +++ b/test/regression/convert/convert_test.go @@ -0,0 +1,120 @@ +/* +## registry+v1 bundle regression test + +This test in convert_test.go verifies that rendering registry+v1 bundles to manifests +always produces the same files and content. + +It runs: go run generate-manifests.go -output-dir=./testdata/tmp/rendered/ +Then compares: ./testdata/tmp/rendered/ vs ./testdata/expected-manifests/ + +Files are sorted by kind/namespace/name for consistency. + +To update expected output (only on purpose), run: + + go run generate-manifests.go -output-dir=./testdata/expected-manifests/ +*/ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" +) + +// Test_RenderedOutputMatchesExpected runs generate-manifests.go, +// then compares the generated files in ./testdata/tmp/rendered/ +// against expected-manifests/. +// It fails if any file differs or is missing. +// TMP dir is cleaned up after test ends. +func Test_RenderedOutput_MatchesExpected(t *testing.T) { + tmpRoot := "./testdata/tmp/rendered/" + expectedRoot := "./testdata/expected-manifests/" + + // Remove the temporary output directory always + t.Cleanup(func() { + _ = os.RemoveAll("./testdata/tmp") + }) + + // Call the generate-manifests.go script to generate the manifests + // in the temporary directory. + cmd := exec.Command("go", "run", "generate-manifests.go", "-output-dir="+tmpRoot) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + err := cmd.Run() + require.NoError(t, err, "failed to generate manifests") + + // Compare structure + contents + err = compareDirs(expectedRoot, tmpRoot) + require.NoError(t, err, "rendered output differs from expected") +} + +// compareDirs compares expectedRootPath and generatedRootPath directories recursively. +// It returns an error if any file is missing, extra, or has content mismatch. +// On mismatch, it includes a detailed diff using cmp.Diff. +func compareDirs(expectedRootPath, generatedRootPath string) error { + // Step 1: Ensure every expected file exists in actual and contents match + err := filepath.Walk(expectedRootPath, func(expectedPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(expectedRootPath, expectedPath) + if err != nil { + return err + } + actualPath := filepath.Join(generatedRootPath, relPath) + + expectedBytes, err := os.ReadFile(expectedPath) + if err != nil { + return fmt.Errorf("failed to read expected file: %s", expectedPath) + } + actualBytes, err := os.ReadFile(actualPath) + if err != nil { + return fmt.Errorf("missing file: %s", relPath) + } + + if !bytes.Equal(expectedBytes, actualBytes) { + diff := cmp.Diff(string(expectedBytes), string(actualBytes)) + return fmt.Errorf("file content mismatch at: %s\nDiff (-expected +actual):\n%s", relPath, diff) + } + return nil + }) + if err != nil { + return err + } + + // Step 2: Ensure actual does not contain unexpected files + err = filepath.Walk(generatedRootPath, func(actualPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(generatedRootPath, actualPath) + if err != nil { + return err + } + expectedPath := filepath.Join(expectedRootPath, relPath) + + _, err = os.Stat(expectedPath) + if os.IsNotExist(err) { + return fmt.Errorf("unexpected extra file: %s", relPath) + } else if err != nil { + return fmt.Errorf("error checking expected file: %s", expectedPath) + } + return nil + }) + return err +} diff --git a/test/convert/generate-manifests.go b/test/regression/convert/generate-manifests.go similarity index 69% rename from test/convert/generate-manifests.go rename to test/regression/convert/generate-manifests.go index 147b05e36d..b2a6565505 100644 --- a/test/convert/generate-manifests.go +++ b/test/regression/convert/generate-manifests.go @@ -1,7 +1,20 @@ +// generate-manifests.go +// +// Renders registry+v1 bundles into YAML manifests for regression testing. +// Used by tests to make sure output from the BundleRenderer stays consistent. +// +// By default, writes to ./testdata/tmp/generate/. +// To update expected output, run: +// +// go run generate-manifests.go -output-dir=./testdata/expected-manifests/ +// +// Only re-generate if you intentionally change rendering behavior. +// Note that if the test fails is likely a regression in the renderer. package main import ( "cmp" + "flag" "fmt" "os" "path/filepath" @@ -16,11 +29,26 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" ) +// This is a helper for a regression test to make sure the renderer output doesn't change. +// +// It renders known bundles into YAML files and writes them to a target output dir. +// By default, it writes to a temp path used in tests: +// +// ./testdata/tmp/rendered/ +// +// If you want to update the expected output, run it with: +// +// go run generate-manifests.go -output-dir=./testdata/expected-manifests/ +// +// Note: Expected output should never change unless the renderer changes which is unlikely. +// If the convert_test.go test fails, it likely means a regression was introduced in the renderer. func main() { bundleRootDir := "testdata/bundles/" - outputRootDir := "test/convert/expected-manifests/" + defaultOutputDir := "./testdata/tmp/rendered/" + outputRootDir := flag.String("output-dir", defaultOutputDir, "path to write rendered manifests to") + flag.Parse() - if err := os.RemoveAll(outputRootDir); err != nil { + if err := os.RemoveAll(*outputRootDir); err != nil { fmt.Printf("error removing output directory: %v\n", err) os.Exit(1) } @@ -52,7 +80,7 @@ func main() { }, } { bundlePath := filepath.Join(bundleRootDir, tc.bundle) - generatedManifestPath := filepath.Join(outputRootDir, tc.bundle, tc.testCaseName) + generatedManifestPath := filepath.Join(*outputRootDir, tc.bundle, tc.testCaseName) if err := generateManifests(generatedManifestPath, bundlePath, tc.installNamespace, tc.watchNamespace); err != nil { fmt.Printf("Error generating manifests: %v", err) os.Exit(1) diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-controller-manager-metrics-service_v1_service.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-controller-manager-metrics-service_v1_service.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-controller-manager-metrics-service_v1_service.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-controller-manager-metrics-service_v1_service.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-manager-config_v1_configmap.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-manager-config_v1_configmap.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-manager-config_v1_configmap.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-manager-config_v1_configmap.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator-metrics-reader_rbac.authorization.k8s.io_v1_clusterrole.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator.v0.6.0.clusterserviceversion.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator.v0.6.0.clusterserviceversion.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator.v0.6.0.clusterserviceversion.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argocd-operator.v0.6.0.clusterserviceversion.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applications.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applications.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applications.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applications.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applicationsets.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applicationsets.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applicationsets.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_applicationsets.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_appprojects.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_appprojects.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_appprojects.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_appprojects.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocdexports.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocdexports.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocdexports.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocdexports.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocds.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocds.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocds.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/manifests/argoproj.io_argocds.yaml diff --git a/testdata/bundles/argocd-operator.v0.6.0/metadata/annotations.yaml b/test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/metadata/annotations.yaml similarity index 100% rename from testdata/bundles/argocd-operator.v0.6.0/metadata/annotations.yaml rename to test/regression/convert/testdata/bundles/argocd-operator.v0.6.0/metadata/annotations.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/00_clusterrole_argocd-operator-metrics-reader.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/00_clusterrole_argocd-operator-metrics-reader.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/00_clusterrole_argocd-operator-metrics-reader.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/00_clusterrole_argocd-operator-metrics-reader.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 98% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 480e2fab02..d90e1d44b5 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx rules: - apiGroups: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml similarity index 95% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml index 26b847f63f..3abef0594a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/02_clusterrole_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p rules: - apiGroups: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 6ac5172d07..e1752b965a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/03_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx roleRef: apiGroup: rbac.authorization.k8s.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml index 486dc11da9..5edc498bbc 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/04_clusterrolebinding_argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0.-3gkm3u8zfarktdile5wekso69zs9bgzb988mhjm0y6p roleRef: apiGroup: rbac.authorization.k8s.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/05_configmap_argocd-operator-manager-config.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/05_configmap_argocd-operator-manager-config.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/05_configmap_argocd-operator-manager-config.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/05_configmap_argocd-operator-manager-config.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml index 136d6fb546..b1f398ec1b 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/06_customresourcedefinition_applications.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applications.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml index 1699bc829b..272bd9e056 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/07_customresourcedefinition_applicationsets.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applicationsets.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml index 8504d6ff02..1ed93a159d 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/08_customresourcedefinition_appprojects.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: appprojects.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml index 8a8b0b0f18..c3248d4740 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/09_customresourcedefinition_argocdexports.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocdexports.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml index 9b86bd9495..426a186d4e 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/10_customresourcedefinition_argocds.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocds.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml index 81b3e78dba..e643a9b820 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/11_deployment_argocd-operator-controller-manager.yaml @@ -1,7 +1,6 @@ apiVersion: apps/v1 kind: Deployment metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system spec: @@ -150,7 +149,6 @@ spec: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/argoproj-labs/argocd-operator support: Argo CD - creationTimestamp: null labels: control-plane: controller-manager spec: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/12_service_argocd-operator-controller-manager-metrics-service.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/12_service_argocd-operator-controller-manager-metrics-service.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/12_service_argocd-operator-controller-manager-metrics-service.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/12_service_argocd-operator-controller-manager-metrics-service.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml similarity index 81% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml index 3c43f503cf..8e5212c47c 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml @@ -1,6 +1,5 @@ apiVersion: v1 kind: ServiceAccount metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 98% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 480e2fab02..d90e1d44b5 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx rules: - apiGroups: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 6ac5172d07..e1752b965a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx roleRef: apiGroup: rbac.authorization.k8s.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/03_configmap_argocd-operator-manager-config.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/03_configmap_argocd-operator-manager-config.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/03_configmap_argocd-operator-manager-config.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/03_configmap_argocd-operator-manager-config.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml index 136d6fb546..b1f398ec1b 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/04_customresourcedefinition_applications.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applications.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml index 1699bc829b..272bd9e056 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applicationsets.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml index 8504d6ff02..1ed93a159d 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: appprojects.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml index 8a8b0b0f18..c3248d4740 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocdexports.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml index 9b86bd9495..426a186d4e 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocds.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml index cbde50aa95..f07080f3f3 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/09_deployment_argocd-operator-controller-manager.yaml @@ -1,7 +1,6 @@ apiVersion: apps/v1 kind: Deployment metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system spec: @@ -150,7 +149,6 @@ spec: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/argoproj-labs/argocd-operator support: Argo CD - creationTimestamp: null labels: control-plane: controller-manager spec: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml similarity index 95% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml index 1c6dd8c8a1..fd60a6dc8c 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - creationTimestamp: null name: argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3 namespace: argocd-system rules: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml index 3119a2b1f3..c3a6e24e8a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3 namespace: argocd-system roleRef: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml similarity index 81% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml index 3c43f503cf..8e5212c47c 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/own-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml @@ -1,6 +1,5 @@ apiVersion: v1 kind: ServiceAccount metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/00_clusterrole_argocd-operator-metrics-reader.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 98% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 480e2fab02..d90e1d44b5 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/01_clusterrole_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx rules: - apiGroups: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml index 6ac5172d07..e1752b965a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/02_clusterrolebinding_argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0-1dhiybrldl1gyksid1dk2dqjsc72psdybc7iyvse5gpx roleRef: apiGroup: rbac.authorization.k8s.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/03_configmap_argocd-operator-manager-config.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/03_configmap_argocd-operator-manager-config.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/03_configmap_argocd-operator-manager-config.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/03_configmap_argocd-operator-manager-config.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml index 136d6fb546..b1f398ec1b 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/04_customresourcedefinition_applications.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applications.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml index 1699bc829b..272bd9e056 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/05_customresourcedefinition_applicationsets.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: applicationsets.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml index 8504d6ff02..1ed93a159d 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/06_customresourcedefinition_appprojects.argoproj.io.yaml @@ -1,7 +1,6 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - creationTimestamp: null labels: app.kubernetes.io/name: appprojects.argoproj.io app.kubernetes.io/part-of: argocd diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml index 8a8b0b0f18..c3248d4740 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/07_customresourcedefinition_argocdexports.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocdexports.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml index 9b86bd9495..426a186d4e 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/own-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/08_customresourcedefinition_argocds.argoproj.io.yaml @@ -3,7 +3,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.6.1 - creationTimestamp: null name: argocds.argoproj.io spec: group: argoproj.io diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml similarity index 99% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml index dac9e7b74e..284bc54774 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/09_deployment_argocd-operator-controller-manager.yaml @@ -1,7 +1,6 @@ apiVersion: apps/v1 kind: Deployment metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system spec: @@ -150,7 +149,6 @@ spec: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/argoproj-labs/argocd-operator support: Argo CD - creationTimestamp: null labels: control-plane: controller-manager spec: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml similarity index 95% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml index 1e152a48f4..cd9667a38a 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/10_role_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - creationTimestamp: null name: argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3 namespace: argocd-watch rules: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml similarity index 93% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml index 4c43e95d4d..6ba70f3e58 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/11_rolebinding_argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3.yaml @@ -1,7 +1,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: - creationTimestamp: null name: argocd-operator.v0-22gmilmgp91wu25is5i2ec598hni8owq3l71bbkl7iz3 namespace: argocd-watch roleRef: diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml similarity index 100% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/single-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/12_service_argocd-operator-controller-manager-metrics-service.yaml diff --git a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml similarity index 81% rename from test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml rename to test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml index 3c43f503cf..8e5212c47c 100644 --- a/test/convert/expected-manifests/argocd-operator.v0.6.0/all-namespaces/13_serviceaccount_argocd-operator-controller-manager.yaml +++ b/test/regression/convert/testdata/expected-manifests/argocd-operator.v0.6.0/single-namespace/13_serviceaccount_argocd-operator-controller-manager.yaml @@ -1,6 +1,5 @@ apiVersion: v1 kind: ServiceAccount metadata: - creationTimestamp: null name: argocd-operator-controller-manager namespace: argocd-system diff --git a/test/upgrade-e2e/post_upgrade_test.go b/test/upgrade-e2e/post_upgrade_test.go index 1d20b4afa8..785d91ea3b 100644 --- a/test/upgrade-e2e/post_upgrade_test.go +++ b/test/upgrade-e2e/post_upgrade_test.go @@ -19,7 +19,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" - "github.com/operator-framework/operator-controller/test/utils" + utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" ) const ( @@ -31,18 +31,18 @@ func TestClusterCatalogUnpacking(t *testing.T) { ctx := context.Background() t.Log("Checking that the controller-manager deployment is updated") - managerLabelSelector := labels.Set{"control-plane": "catalogd-controller-manager"} + managerLabelSelector := labels.Set{"app.kubernetes.io/name": "catalogd"} var managerDeployment appsv1.Deployment require.EventuallyWithT(t, func(ct *assert.CollectT) { var managerDeployments appsv1.DeploymentList err := c.List(ctx, &managerDeployments, client.MatchingLabels(managerLabelSelector), client.InNamespace("olmv1-system")) - assert.NoError(ct, err) - assert.Len(ct, managerDeployments.Items, 1) + require.NoError(ct, err) + require.Len(ct, managerDeployments.Items, 1) managerDeployment = managerDeployments.Items[0] - assert.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.UpdatedReplicas) - assert.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.Replicas) - assert.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.AvailableReplicas) - assert.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.ReadyReplicas) + require.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.UpdatedReplicas) + require.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.Replicas) + require.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.AvailableReplicas) + require.Equal(ct, *managerDeployment.Spec.Replicas, managerDeployment.Status.ReadyReplicas) }, time.Minute, time.Second) var managerPod corev1.Pod @@ -50,8 +50,8 @@ func TestClusterCatalogUnpacking(t *testing.T) { require.EventuallyWithT(t, func(ct *assert.CollectT) { var managerPods corev1.PodList err := c.List(ctx, &managerPods, client.MatchingLabels(managerLabelSelector)) - assert.NoError(ct, err) - assert.Len(ct, managerPods.Items, 1) + require.NoError(ct, err) + require.Len(ct, managerPods.Items, 1) managerPod = managerPods.Items[0] }, time.Minute, time.Second) @@ -78,21 +78,21 @@ func TestClusterCatalogUnpacking(t *testing.T) { t.Log("Ensuring ClusterCatalog has Status.Condition of Progressing with a status == True, reason == Succeeded") require.EventuallyWithT(t, func(ct *assert.CollectT) { err := c.Get(ctx, types.NamespacedName{Name: testClusterCatalogName}, catalog) - assert.NoError(ct, err) + require.NoError(ct, err) cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeProgressing) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) }, time.Minute, time.Second) t.Log("Ensuring ClusterCatalog has Status.Condition of Serving with a status == True, reason == Available") require.EventuallyWithT(t, func(ct *assert.CollectT) { err := c.Get(ctx, types.NamespacedName{Name: testClusterCatalogName}, catalog) - assert.NoError(ct, err) + require.NoError(ct, err) cond := apimeta.FindStatusCondition(catalog.Status.Conditions, ocv1.TypeServing) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) }, time.Minute, time.Second) } @@ -103,11 +103,11 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { // wait for catalogd deployment to finish t.Log("Wait for catalogd deployment to be ready") - catalogdManagerPod := waitForDeployment(t, ctx, "catalogd-controller-manager") + catalogdManagerPod := waitForDeployment(t, ctx, "catalogd") // wait for operator-controller deployment to finish t.Log("Wait for operator-controller deployment to be ready") - managerPod := waitForDeployment(t, ctx, "operator-controller-controller-manager") + managerPod := waitForDeployment(t, ctx, "operator-controller") t.Log("Wait for acquired leader election") // Average case is under 1 minute but in the worst case: (previous leader crashed) @@ -135,13 +135,13 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { t.Log("Checking that the ClusterCatalog is unpacked") require.EventuallyWithT(t, func(ct *assert.CollectT) { var clusterCatalog ocv1.ClusterCatalog - assert.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterCatalogName}, &clusterCatalog)) + require.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterCatalogName}, &clusterCatalog)) // check serving condition cond := apimeta.FindStatusCondition(clusterCatalog.Status.Conditions, ocv1.TypeServing) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) // mitigation for upgrade-e2e flakiness caused by the following bug // https://github.com/operator-framework/operator-controller/issues/1626 @@ -150,24 +150,28 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { if cond == nil { return } - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.True(ct, clusterCatalog.Status.LastUnpacked.After(catalogdManagerPod.CreationTimestamp.Time)) + require.True(ct, clusterCatalog.Status.LastUnpacked.After(catalogdManagerPod.CreationTimestamp.Time)) }, time.Minute, time.Second) + // TODO: if we change the underlying revision storage mechanism, the new version + // will not detect any installed versions, we need to make sure that the upgrade + // test fails across revision storage mechanism changes that are not also accompanied + // by code that automatically migrates the revision storage. + t.Log("Checking that the ClusterExtension is installed") var clusterExtension ocv1.ClusterExtension require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterExtensionName}, &clusterExtension)) + require.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterExtensionName}, &clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - assert.NotNil(ct, cond) - assert.Equal(ct, metav1.ConditionTrue, cond.Status) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - if assert.NotNil(ct, clusterExtension.Status.Install) { - assert.NotEmpty(ct, clusterExtension.Status.Install.Bundle.Version) - } + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotNil(ct, clusterExtension.Status.Install) + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle.Version) }, time.Minute, time.Second) previousVersion := clusterExtension.Status.Install.Bundle.Version @@ -179,32 +183,32 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { t.Log("Checking that the ClusterExtension installs successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterExtensionName}, &clusterExtension)) + require.NoError(ct, c.Get(ctx, types.NamespacedName{Name: testClusterExtensionName}, &clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) - assert.NotNil(ct, cond) - assert.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - assert.Contains(ct, cond.Message, "Installed bundle") - assert.Equal(ct, ocv1.BundleMetadata{Name: "test-operator.1.0.1", Version: "1.0.1"}, clusterExtension.Status.Install.Bundle) - assert.NotEqual(ct, previousVersion, clusterExtension.Status.Install.Bundle.Version) + require.NotNil(ct, cond) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.Equal(ct, ocv1.BundleMetadata{Name: "test-operator.1.0.1", Version: "1.0.1"}, clusterExtension.Status.Install.Bundle) + require.NotEqual(ct, previousVersion, clusterExtension.Status.Install.Bundle.Version) }, time.Minute, time.Second) } -// waitForDeployment checks that the updated deployment with the given control-plane label +// waitForDeployment checks that the updated deployment with the given app.kubernetes.io/name label // has reached the desired number of replicas and that the number pods matches that number // i.e. no old pods remain. It will return a pointer to the first pod. This is only necessary // to facilitate the mitigation put in place for https://github.com/operator-framework/operator-controller/issues/1626 func waitForDeployment(t *testing.T, ctx context.Context, controlPlaneLabel string) *corev1.Pod { - deploymentLabelSelector := labels.Set{"control-plane": controlPlaneLabel}.AsSelector() + deploymentLabelSelector := labels.Set{"app.kubernetes.io/name": controlPlaneLabel}.AsSelector() t.Log("Checking that the deployment is updated") var desiredNumReplicas int32 require.EventuallyWithT(t, func(ct *assert.CollectT) { var managerDeployments appsv1.DeploymentList - assert.NoError(ct, c.List(ctx, &managerDeployments, client.MatchingLabelsSelector{Selector: deploymentLabelSelector})) - assert.Len(ct, managerDeployments.Items, 1) + require.NoError(ct, c.List(ctx, &managerDeployments, client.MatchingLabelsSelector{Selector: deploymentLabelSelector})) + require.Len(ct, managerDeployments.Items, 1) managerDeployment := managerDeployments.Items[0] - assert.True(ct, + require.True(ct, managerDeployment.Status.UpdatedReplicas == *managerDeployment.Spec.Replicas && managerDeployment.Status.Replicas == *managerDeployment.Spec.Replicas && managerDeployment.Status.AvailableReplicas == *managerDeployment.Spec.Replicas && @@ -216,8 +220,8 @@ func waitForDeployment(t *testing.T, ctx context.Context, controlPlaneLabel stri var managerPods corev1.PodList t.Logf("Ensure the number of remaining pods equal the desired number of replicas (%d)", desiredNumReplicas) require.EventuallyWithT(t, func(ct *assert.CollectT) { - assert.NoError(ct, c.List(ctx, &managerPods, client.MatchingLabelsSelector{Selector: deploymentLabelSelector})) - assert.Len(ct, managerPods.Items, 1) + require.NoError(ct, c.List(ctx, &managerPods, client.MatchingLabelsSelector{Selector: deploymentLabelSelector})) + require.Len(ct, managerPods.Items, 1) }, time.Minute, time.Second) return &managerPods.Items[0] } diff --git a/test/utils.go b/test/utils.go new file mode 100644 index 0000000000..22a50b2b8d --- /dev/null +++ b/test/utils.go @@ -0,0 +1,54 @@ +package test + +import ( + "os" + "path" + "path/filepath" + "runtime" + + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +// NewEnv creates a new envtest.Environment instance. +func NewEnv() *envtest.Environment { + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{ + pathFromProjectRoot("helm/olmv1/base/operator-controller/crd/experimental"), + }, + ErrorIfCRDPathMissing: true, + } + // ENVTEST-based tests require specific binaries. By default, these binaries are located + // in paths defined by controller-runtime. However, the `BinaryAssetsDirectory` needs + // to be explicitly set when running tests directly (e.g., debugging tests in an IDE) + // without using the Makefile targets. + // + // This is equivalent to configuring your IDE to export the `KUBEBUILDER_ASSETS` environment + // variable before each test execution. The following function simplifies this process + // by handling the configuration for you. + // + // To ensure the binaries are in the expected path without manual configuration, run: + // `make envtest-k8s-bins` + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + return testEnv +} + +// pathFromProjectRoot returns the absolute path to the given relative path from the project root. +func pathFromProjectRoot(relativePath string) string { + _, filename, _, _ := runtime.Caller(0) + p := path.Join(path.Dir(path.Dir(filename)), relativePath) + return p +} + +// getFirstFoundEnvTestBinaryDir finds and returns the first directory under the given path. +func getFirstFoundEnvTestBinaryDir() string { + basePath := pathFromProjectRoot(filepath.Join("bin", "envtest-binaries", "k8s")) + entries, _ := os.ReadDir(basePath) + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/test/utils/utils.go b/test/utils/utils.go deleted file mode 100644 index 1acc55fe6b..0000000000 --- a/test/utils/utils.go +++ /dev/null @@ -1,69 +0,0 @@ -package utils - -import ( - "context" - "fmt" - "io" - "net/url" - "os/exec" - "strings" - "testing" - - "k8s.io/client-go/kubernetes" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" -) - -// FindK8sClient returns the first available Kubernetes CLI client from the system, -// It checks for the existence of each client by running `version --client`. -// If no suitable client is found, the function terminates the test with a failure. -func FindK8sClient(t *testing.T) string { - t.Logf("Finding kubectl client") - clients := []string{"kubectl", "oc"} - for _, c := range clients { - // Would prefer to use `command -v`, but even that may not be installed! - if err := exec.Command(c, "version", "--client").Run(); err == nil { - t.Logf("Using %q as k8s client", c) - return c - } - } - t.Fatal("k8s client not found") - return "" -} - -func ReadTestCatalogServerContents(ctx context.Context, catalog *ocv1.ClusterCatalog, kubeClient kubernetes.Interface) ([]byte, error) { - if catalog == nil { - return nil, fmt.Errorf("cannot read nil catalog") - } - if catalog.Status.URLs == nil { - return nil, fmt.Errorf("catalog %q has no catalog urls", catalog.Name) - } - url, err := url.Parse(catalog.Status.URLs.Base) - if err != nil { - return nil, fmt.Errorf("error parsing clustercatalog url %q: %v", catalog.Status.URLs.Base, err) - } - // url is expected to be in the format of - // http://{service_name}.{namespace}.svc/catalogs/{catalog_name}/ - // so to get the namespace and name of the service we grab only - // the hostname and split it on the '.' character - ns := strings.Split(url.Hostname(), ".")[1] - name := strings.Split(url.Hostname(), ".")[0] - port := url.Port() - // the ProxyGet() call below needs an explicit port value, so if - // value from url.Port() is empty, we assume port 443. - if port == "" { - if url.Scheme == "https" { - port = "443" - } else { - port = "80" - } - } - resp := kubeClient.CoreV1().Services(ns).ProxyGet(url.Scheme, name, port, url.JoinPath("api", "v1", "all").Path, map[string]string{}) - rc, err := resp.Stream(ctx) - if err != nil { - return nil, err - } - defer rc.Close() - - return io.ReadAll(rc) -} diff --git a/testdata/.gitignore b/testdata/.gitignore index 1eca1dc7e4..8e0dcaba17 100644 --- a/testdata/.gitignore +++ b/testdata/.gitignore @@ -1,2 +1 @@ push/bin -registry/bin diff --git a/testdata/Dockerfile b/testdata/Dockerfile index 2868542e61..0bee4012bf 100644 --- a/testdata/Dockerfile +++ b/testdata/Dockerfile @@ -2,11 +2,8 @@ FROM gcr.io/distroless/static:nonroot WORKDIR / -COPY registry/bin/registry registry COPY push/bin/push push COPY images images -EXPOSE 5000 - USER 65532:65532 diff --git a/testdata/build-test-registry.sh b/testdata/build-test-registry.sh index 8b13720212..e2dcc09148 100755 --- a/testdata/build-test-registry.sh +++ b/testdata/build-test-registry.sh @@ -45,6 +45,7 @@ spec: - ${name}-controller-manager-metrics-service.${namespace}.svc - ${name}-controller-manager-metrics-service.${namespace}.svc.cluster.local privateKey: + rotationPolicy: Always algorithm: ECDSA size: 256 issuerRef: @@ -71,12 +72,8 @@ spec: spec: containers: - name: registry - image: ${image} + image: registry:3 imagePullPolicy: IfNotPresent - command: - - /registry - args: - - "--registry-address=:5000" volumeMounts: - name: certs-vol mountPath: "/certs" diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml new file mode 100644 index 0000000000..305e3c73e8 --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: ownnamespaces.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: OwnNamespace + listKind: OwnNamespaceList + plural: ownnamespaces + singular: ownnamespace + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + testField: + type: string diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml new file mode 100644 index 0000000000..d6c2a32550 --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "ownnamespaces.olm.operatorframework.io/v1", + "kind": "OwnNamespace", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: ownnamespaceoperator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: A dummy resource for an operator that only supports own namespace install mode + displayName: OwnNamespace + kind: OwnNamespace + name: ownnamespaces.olm.operatorframework.io + version: v1 + description: OLM OwnNamespace Testing Operator + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: own-namespace-operator + app.kubernetes.io/version: 1.0.0 + name: own-namespace-operator + spec: + replicas: 1 + selector: + matchLabels: + app: ownnamespacetest + template: + metadata: + labels: + app: ownnamespacetest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.36 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.0.0 diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml new file mode 100644 index 0000000000..24cf5213a1 --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml @@ -0,0 +1,10 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: own-namespace-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: unknown diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml new file mode 100644 index 0000000000..5f8d305434 --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: singlenamespaces.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: SingleNamespace + listKind: SingleNamespaceList + plural: singlenamespaces + singular: singlenamespace + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + testField: + type: string diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml new file mode 100644 index 0000000000..dea4c7f09c --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "singlenamespaces.olm.operatorframework.io/v1", + "kind": "SingleNamespace", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: singlenamespaceoperator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: A dummy resource for an operator that only supports single namespace install mode + displayName: SingleNamespace + kind: SingleNamespace + name: singlenamespaces.olm.operatorframework.io + version: v1 + description: OLM SingleNamespace Testing Operator + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: single-namespace-operator + app.kubernetes.io/version: 1.0.0 + name: single-namespace-operator + spec: + replicas: 1 + selector: + matchLabels: + app: singlenamespacetest + template: + metadata: + labels: + app: singlenamespacetest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.36 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.0.0 diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml new file mode 100644 index 0000000000..b7d60e6df0 --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml @@ -0,0 +1,10 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: single-namespace-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: unknown diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/bundle.configmap.yaml index 219655ab50..74a3623ee7 100644 --- a/testdata/images/bundles/test-operator/v1.0.0/manifests/bundle.configmap.yaml +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/bundle.configmap.yaml @@ -2,6 +2,10 @@ apiVersion: v1 kind: ConfigMap metadata: name: test-configmap + annotations: + shouldNotTemplate: > + The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles. + data: version: "v1.0.0" name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml index fcfd4aeafe..44e64cef79 100644 --- a/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/olm.operatorframework.com_olme2etest.yaml @@ -1,4 +1,3 @@ ---- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml index bdefe11fe8..4cb49d5fe0 100644 --- a/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.clusterserviceversion.yaml @@ -35,105 +35,115 @@ spec: description: OLM E2E Testing Operator displayName: test-operator icon: - - base64data: "" - mediatype: "" + - base64data: "" + mediatype: "" install: spec: deployments: - - label: - app.kubernetes.io/component: controller - app.kubernetes.io/name: test-operator - app.kubernetes.io/version: 1.0.0 - name: test-operator - spec: - replicas: 1 - selector: - matchLabels: - app: olme2etest - template: - metadata: - labels: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: test-operator + app.kubernetes.io/version: 1.0.0 + name: test-operator + spec: + replicas: 1 + selector: + matchLabels: app: olme2etest - spec: - terminationGracePeriodSeconds: 0 - containers: - - name: busybox - image: busybox - command: - - 'sleep' - - '1000' - securityContext: - runAsUser: 1000 - runAsNonRoot: true - serviceAccountName: simple-bundle-manager + template: + metadata: + labels: + app: olme2etest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.36 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager clusterPermissions: - - rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: simple-bundle-manager + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - - serviceaccounts - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - serviceAccountName: simple-bundle-manager + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager strategy: deployment installModes: - - supported: false - type: OwnNamespace - - supported: false - type: SingleNamespace - - supported: false - type: MultiNamespace - - supported: true - type: AllNamespaces + - supported: false + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces keywords: - - registry + - registry links: - - name: simple-bundle - url: https://simple-bundle.domain + - name: simple-bundle + url: https://simple-bundle.domain maintainers: - - email: main#simple-bundle.domain - name: Simple Bundle + - email: main#simple-bundle.domain + name: Simple Bundle maturity: beta provider: name: Simple Bundle diff --git a/config/base/common/network_policy.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.networkpolicy.yaml similarity index 53% rename from config/base/common/network_policy.yaml rename to testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.networkpolicy.yaml index 86d3529751..20a5ea834f 100644 --- a/config/base/common/network_policy.yaml +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/testoperator.networkpolicy.yaml @@ -1,11 +1,8 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: default-deny-all-traffic - namespace: system + name: test-operator-network-policy spec: - podSelector: { } + podSelector: {} policyTypes: - Ingress - - Egress - diff --git a/testdata/images/bundles/test-operator/v1.2.0/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.2.0/manifests/bundle.configmap.yaml new file mode 100644 index 0000000000..22ee500908 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.2.0/manifests/bundle.configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap + annotations: + shouldNotTemplate: > + The namespace is {{ $labels.namespace }}. The templated $labels.namespace is NOT expected to be processed by OLM's rendering engine for registry+v1 bundles. + +data: + version: "v1.2.0" + name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml b/testdata/images/bundles/test-operator/v1.2.0/manifests/olm.operatorframework.com_olme2etest.yaml similarity index 99% rename from testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml rename to testdata/images/bundles/test-operator/v1.2.0/manifests/olm.operatorframework.com_olme2etest.yaml index fcfd4aeafe..44e64cef79 100644 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/olm.operatorframework.com_olme2etest.yaml +++ b/testdata/images/bundles/test-operator/v1.2.0/manifests/olm.operatorframework.com_olme2etest.yaml @@ -1,4 +1,3 @@ ---- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: diff --git a/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.clusterserviceversion.yaml new file mode 100644 index 0000000000..90621bc6d4 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "olme2etests.olm.operatorframework.io/v1", + "kind": "OLME2ETests", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: testoperator.v1.2.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: Configures subsections of Alertmanager configuration specific to each namespace + displayName: OLME2ETest + kind: OLME2ETest + name: olme2etests.olm.operatorframework.io + version: v1 + description: OLM E2E Testing Operator + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: test-operator + app.kubernetes.io/version: 1.2.0 + name: test-operator + spec: + replicas: 1 + selector: + matchLabels: + app: olme2etest + template: + metadata: + labels: + app: olme2etest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.37 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.2.0 diff --git a/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.networkpolicy.yaml b/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.networkpolicy.yaml new file mode 100644 index 0000000000..20a5ea834f --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.2.0/manifests/testoperator.networkpolicy.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-operator-network-policy +spec: + podSelector: {} + policyTypes: + - Ingress diff --git a/testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.2.0/metadata/annotations.yaml similarity index 100% rename from testdata/images/bundles/test-operator/v2.0.0/metadata/annotations.yaml rename to testdata/images/bundles/test-operator/v1.2.0/metadata/annotations.yaml diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml deleted file mode 100644 index ef17ce45ea..0000000000 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/bundle.configmap.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: test-configmap - annotations: - shouldNotTemplate: > - The namespace is {{ $labels.namespace }}. The templated - $labels.namespace is NOT expected to be processed by OLM's - rendering engine for registry+v1 bundles. -data: - version: "v2.0.0" - name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml deleted file mode 100644 index a375c19010..0000000000 --- a/testdata/images/bundles/test-operator/v2.0.0/manifests/testoperator.clusterserviceversion.yaml +++ /dev/null @@ -1,141 +0,0 @@ -apiVersion: operators.coreos.com/v1alpha1 -kind: ClusterServiceVersion -metadata: - annotations: - alm-examples: |- - [ - { - "apiVersion": "olme2etests.olm.operatorframework.io/v1", - "kind": "OLME2ETests", - "metadata": { - "labels": { - "app.kubernetes.io/managed-by": "kustomize", - "app.kubernetes.io/name": "test" - }, - "name": "test-sample" - }, - "spec": null - } - ] - capabilities: Basic Install - createdAt: "2024-10-24T19:21:40Z" - operators.operatorframework.io/builder: operator-sdk-v1.34.1 - operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: testoperator.v2.0.0 - namespace: placeholder -spec: - apiservicedefinitions: {} - customresourcedefinitions: - owned: - - description: Configures subsections of Alertmanager configuration specific to each namespace - displayName: OLME2ETest - kind: OLME2ETest - name: olme2etests.olm.operatorframework.io - version: v1 - description: OLM E2E Testing Operator - displayName: test-operator - icon: - - base64data: "" - mediatype: "" - install: - spec: - deployments: - - label: - app.kubernetes.io/component: controller - app.kubernetes.io/name: test-operator - app.kubernetes.io/version: 2.0.0 - name: test-operator - spec: - replicas: 1 - selector: - matchLabels: - app: olme2etest - template: - metadata: - labels: - app: olme2etest - spec: - terminationGracePeriodSeconds: 0 - containers: - - name: busybox - image: busybox - command: - - 'sleep' - - '1000' - securityContext: - runAsUser: 1000 - runAsNonRoot: true - serviceAccountName: simple-bundle-manager - clusterPermissions: - - rules: - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create - serviceAccountName: simple-bundle-manager - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - - serviceaccounts - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - serviceAccountName: simple-bundle-manager - strategy: deployment - installModes: - - supported: false - type: OwnNamespace - - supported: false - type: SingleNamespace - - supported: false - type: MultiNamespace - - supported: true - type: AllNamespaces - keywords: - - registry - links: - - name: simple-bundle - url: https://simple-bundle.domain - maintainers: - - email: main#simple-bundle.domain - name: Simple Bundle - maturity: beta - provider: - name: Simple Bundle - url: https://simple-bundle.domain - version: 2.0.0 diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml new file mode 100644 index 0000000000..2394392b68 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator-metrics-reader_rbac.authorization.k8s.io_v1beta1_clusterrole.yaml @@ -0,0 +1,10 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: webhook-operator-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml new file mode 100644 index 0000000000..902ce0ca91 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook-operator.clusterserviceversion.yaml @@ -0,0 +1,281 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "webhook.operators.coreos.io/v1", + "kind": "WebhookTest", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "webhook-operator" + }, + "name": "webhooktest-sample" + }, + "spec": null + }, + { + "apiVersion": "webhook.operators.coreos.io/v2", + "kind": "WebhookTest", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "webhook-operator" + }, + "name": "webhooktest-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2025-09-04T14:17:32Z" + operators.operatorframework.io/builder: operator-sdk-v1.40.0+git + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: webhook-operator.v0.0.1 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: WebhookTest is the Schema for the webhooktests API + displayName: Webhook Test + kind: WebhookTest + name: webhooktests.webhook.operators.coreos.io + version: v1 + - description: WebhookTest is the Schema for the webhooktests API + displayName: Webhook Test + kind: WebhookTest + name: webhooktests.webhook.operators.coreos.io + version: v2 + description: Webhook Operator description. TODO. + displayName: Webhook Operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + clusterPermissions: + - rules: + - apiGroups: + - webhook.operators.coreos.io + resources: + - webhooktests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - webhook.operators.coreos.io + resources: + - webhooktests/finalizers + verbs: + - update + - apiGroups: + - webhook.operators.coreos.io + resources: + - webhooktests/status + verbs: + - get + - patch + - update + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: webhook-operator-controller-manager + deployments: + - label: + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: webhook-operator + control-plane: controller-manager + name: webhook-operator-controller-manager + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: webhook-operator + control-plane: controller-manager + strategy: {} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + app.kubernetes.io/name: webhook-operator + control-plane: controller-manager + spec: + containers: + - args: + - --leader-elect + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + command: + - /manager + image: quay.io/olmtest/webhook-operator:0.0.4 + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: webhook-operator-controller-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: webhook-certs + secret: + secretName: webhook-server-cert + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: webhook-operator-controller-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + keywords: + - webhook-operator + links: + - name: Webhook Operator + url: https://webhook-operator.domain + maintainers: + - email: your@email.com + name: Maintainer Name + maturity: alpha + provider: + name: Provider Name + url: https://your.domain + version: 0.0.1 + webhookdefinitions: + - admissionReviewVersions: + - v1 + containerPort: 443 + conversionCRDs: + - webhooktests.webhook.operators.coreos.io + deploymentName: webhook-operator-controller-manager + generateName: cwebhooktests.kb.io + sideEffects: None + targetPort: 9443 + type: ConversionWebhook + webhookPath: /convert + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: webhook-operator-controller-manager + failurePolicy: Fail + generateName: mwebhooktest-v1.kb.io + rules: + - apiGroups: + - webhook.operators.coreos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - webhooktests + sideEffects: None + targetPort: 9443 + type: MutatingAdmissionWebhook + webhookPath: /mutate-webhook-operators-coreos-io-v1-webhooktest + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: webhook-operator-controller-manager + failurePolicy: Fail + generateName: vwebhooktest-v1.kb.io + rules: + - apiGroups: + - webhook.operators.coreos.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - webhooktests + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-webhook-operators-coreos-io-v1-webhooktest diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml new file mode 100644 index 0000000000..6d82f2c962 --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/manifests/webhook.operators.coreos.io_webhooktests.yaml @@ -0,0 +1,264 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: webhooktests.webhook.operators.coreos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: webhook-operator-webhook-service + namespace: webhook-operator-system + path: /convert + conversionReviewVersions: + - v1 + group: webhook.operators.coreos.io + names: + kind: WebhookTest + listKind: WebhookTestList + plural: webhooktests + singular: webhooktest + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: WebhookTest is the Schema for the webhooktests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of WebhookTest + properties: + mutate: + description: Mutate is a field that will be set to true by the mutating webhook. + type: boolean + valid: + description: Valid must be set to true or the validation webhook will reject the resource. + type: boolean + required: + - valid + type: object + status: + description: status defines the observed state of WebhookTest + properties: + conditions: + description: |- + conditions represent the current state of the WebhookTest resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - name: v2 + schema: + openAPIV3Schema: + description: WebhookTest is the Schema for the webhooktests API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of WebhookTest + properties: + conversion: + description: Conversion is an example field of WebhookTest. Edit WebhookTest_types.go to remove/update + properties: + mutate: + description: Mutate is a field that will be set to true by the mutating webhook. + type: boolean + valid: + description: Valid must be set to true or the validation webhook will reject the resource. + type: boolean + required: + - valid + type: object + required: + - conversion + type: object + status: + description: status defines the observed state of WebhookTest + properties: + conditions: + description: |- + conditions represent the current state of the WebhookTest resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml b/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml new file mode 100644 index 0000000000..e59521886d --- /dev/null +++ b/testdata/images/bundles/webhook-operator/v0.0.1/metadata/annotations.yaml @@ -0,0 +1,12 @@ +annotations: + operators.operatorframework.io.bundle.channel.default.v1: "" + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: webhook-operator + operators.operatorframework.io.metrics.builder: operator-sdk-v1.0.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go + operators.operatorframework.io.test.config.v1: tests/scorecard/ + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml index 5768705954..437175a4e3 100644 --- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml @@ -1,4 +1,3 @@ ---- schema: olm.package name: test defaultChannel: beta @@ -42,13 +41,12 @@ properties: schema: olm.bundle name: test-operator.1.2.0 package: test -image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.0 +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.2.0 properties: - type: olm.package value: packageName: test version: 1.2.0 - --- schema: olm.package name: test-mirrored @@ -89,3 +87,63 @@ properties: value: packageName: dynamic version: 1.2.0 +--- +schema: olm.package +name: webhook-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: webhook-operator +entries: + - name: webhook-operator.v0.0.1 +--- +schema: olm.bundle +name: webhook-operator.v0.0.1 +package: webhook-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/webhook-operator:v0.0.1 +properties: + - type: olm.package + value: + packageName: webhook-operator + version: 0.0.1 +--- +schema: olm.package +name: own-namespace-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: own-namespace-operator +entries: + - name: own-namespace-operator.1.0.0 +--- +schema: olm.bundle +name: own-namespace-operator.1.0.0 +package: own-namespace-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/own-namespace-operator:v1.0.0 +properties: + - type: olm.package + value: + packageName: own-namespace-operator + version: 1.0.0 +--- +schema: olm.package +name: single-namespace-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: single-namespace-operator +entries: + - name: single-namespace-operator.1.0.0 +--- +schema: olm.bundle +name: single-namespace-operator.1.0.0 +package: single-namespace-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/single-namespace-operator:v1.0.0 +properties: + - type: olm.package + value: + packageName: single-namespace-operator + version: 1.0.0 diff --git a/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml index 821e7631b8..2e82b12290 100644 --- a/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v2/configs/catalog.yaml @@ -1,4 +1,3 @@ ---- schema: olm.package name: test defaultChannel: beta @@ -7,15 +6,15 @@ schema: olm.channel name: beta package: test entries: - - name: test-operator.2.0.0 + - name: test-operator.1.3.0 replaces: test-operator.1.2.0 --- schema: olm.bundle -name: test-operator.2.0.0 +name: test-operator.1.3.0 package: test -image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v2.0.0 +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.0 properties: - type: olm.package value: packageName: test - version: 2.0.0 + version: 1.3.0 diff --git a/testdata/registry/README.md b/testdata/registry/README.md deleted file mode 100644 index 18c41722a7..0000000000 --- a/testdata/registry/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Test Registry - -This tool is a bare-bones image registry using the `go-containerregistry` library; it is intended to be used in a test environment only. - -Usage: -``` -Usage of registry: - --registry-address string The address the registry binds to. (default ":12345") -``` - -The server key and cert locations should be set under the following environment variables: -``` - REGISTRY_HTTP_TLS_CERTIFICATE - REGISTRY_HTTP_TLS_KEY -``` diff --git a/testdata/registry/go.mod b/testdata/registry/go.mod deleted file mode 100644 index ce79002d4b..0000000000 --- a/testdata/registry/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module registry - -go 1.22.5 - -require ( - github.com/google/go-containerregistry v0.20.2 - github.com/spf13/pflag v1.0.5 -) diff --git a/testdata/registry/go.sum b/testdata/registry/go.sum deleted file mode 100644 index ebadf4aec1..0000000000 --- a/testdata/registry/go.sum +++ /dev/null @@ -1,36 +0,0 @@ -github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= -github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= -github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= -github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= -github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= -github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= -github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= -github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= diff --git a/testdata/registry/registry.go b/testdata/registry/registry.go deleted file mode 100644 index 553d9bcd43..0000000000 --- a/testdata/registry/registry.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "flag" - "log" - "net/http" - "os" - "time" - - "github.com/google/go-containerregistry/pkg/registry" - "github.com/spf13/pflag" -) - -const ( - certEnv = "REGISTRY_HTTP_TLS_CERTIFICATE" - keyEnv = "REGISTRY_HTTP_TLS_KEY" -) - -func main() { - var ( - registryAddr string - ) - flag.StringVar(®istryAddr, "registry-address", ":12345", "The address the registry binds to.") - pflag.CommandLine.AddGoFlagSet(flag.CommandLine) - pflag.Parse() - - s := &http.Server{ - Addr: registryAddr, - Handler: registry.New(), - ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, - } - - err := s.ListenAndServeTLS(os.Getenv(certEnv), os.Getenv(keyEnv)) - if err != nil { - log.Fatalf("failed to start image registry: %s", err.Error()) - } - - defer s.Close() -}