diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 8c08ceade4..cfca431657 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -23,11 +23,17 @@ $(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.19.0 +CONFTEST := $(GOBIN)/conftest-v0.62.0 +$(CONFTEST): $(BINGO_DIR)/conftest.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)/conftest-v0.62.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=conftest.mod -o=$(GOBIN)/conftest-v0.62.0 "github.com/open-policy-agent/conftest" + +CONTROLLER_GEN := $(GOBIN)/controller-gen-v0.20.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.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" + @echo "(re)installing $(GOBIN)/controller-gen-v0.20.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=controller-gen.mod -o=$(GOBIN)/controller-gen-v0.20.0 "sigs.k8s.io/controller-tools/cmd/controller-gen" CRD_DIFF := $(GOBIN)/crd-diff-v0.5.0 $(CRD_DIFF): $(BINGO_DIR)/crd-diff.mod @@ -35,11 +41,11 @@ $(CRD_DIFF): $(BINGO_DIR)/crd-diff.mod @echo "(re)installing $(GOBIN)/crd-diff-v0.5.0" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-diff.mod -o=$(GOBIN)/crd-diff-v0.5.0 "sigs.k8s.io/crdify" -CRD_REF_DOCS := $(GOBIN)/crd-ref-docs-v0.2.0 +CRD_REF_DOCS := $(GOBIN)/crd-ref-docs-v0.3.0 $(CRD_REF_DOCS): $(BINGO_DIR)/crd-ref-docs.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)/crd-ref-docs-v0.2.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-ref-docs.mod -o=$(GOBIN)/crd-ref-docs-v0.2.0 "github.com/elastic/crd-ref-docs" + @echo "(re)installing $(GOBIN)/crd-ref-docs-v0.3.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-ref-docs.mod -o=$(GOBIN)/crd-ref-docs-v0.3.0 "github.com/elastic/crd-ref-docs" GOJQ := $(GOBIN)/gojq-v0.12.17 $(GOJQ): $(BINGO_DIR)/gojq.mod @@ -47,17 +53,17 @@ $(GOJQ): $(BINGO_DIR)/gojq.mod @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.6.2 +GOLANGCI_LINT := $(GOBIN)/golangci-lint-v2.8.0 $(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/golangci-lint-v2.6.2" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v2.6.2 "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" + @echo "(re)installing $(GOBIN)/golangci-lint-v2.8.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=golangci-lint.mod -o=$(GOBIN)/golangci-lint-v2.8.0 "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" -GORELEASER := $(GOBIN)/goreleaser-v1.26.2 +GORELEASER := $(GOBIN)/goreleaser-v2.11.2 $(GORELEASER): $(BINGO_DIR)/goreleaser.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)/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" + @echo "(re)installing $(GOBIN)/goreleaser-v2.11.2" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=goreleaser.mod -o=$(GOBIN)/goreleaser-v2.11.2 "github.com/goreleaser/goreleaser/v2" HELM := $(GOBIN)/helm-v3.18.4 $(HELM): $(BINGO_DIR)/helm.mod @@ -65,11 +71,17 @@ $(HELM): $(BINGO_DIR)/helm.mod @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 := $(GOBIN)/kind-v0.31.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.30.0" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.30.0 "sigs.k8s.io/kind" + @echo "(re)installing $(GOBIN)/kind-v0.31.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kind.mod -o=$(GOBIN)/kind-v0.31.0 "sigs.k8s.io/kind" + +KUBE_SCORE := $(GOBIN)/kube-score-v1.20.0 +$(KUBE_SCORE): $(BINGO_DIR)/kube-score.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)/kube-score-v1.20.0" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=kube-score.mod -o=$(GOBIN)/kube-score-v1.20.0 "github.com/zegl/kube-score/cmd/kube-score" KUSTOMIZE := $(GOBIN)/kustomize-v5.7.1 $(KUSTOMIZE): $(BINGO_DIR)/kustomize.mod diff --git a/.bingo/conftest.mod b/.bingo/conftest.mod new file mode 100644 index 0000000000..294b93132a --- /dev/null +++ b/.bingo/conftest.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.24.6 + +require github.com/open-policy-agent/conftest v0.62.0 diff --git a/.bingo/conftest.sum b/.bingo/conftest.sum new file mode 100644 index 0000000000..b34a3b44bc --- /dev/null +++ b/.bingo/conftest.sum @@ -0,0 +1,2041 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.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= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go 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 v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= +cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= +cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= +cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cuelang.org/go v0.10.0 h1:Y1Pu4wwga5HkXfLFK1sWAYaSWIBdcsr5Cb5AWj2pOuE= +cuelang.org/go v0.10.0/go.mod h1:HzlaqqqInHNiqE6slTP6+UtxT9hN6DAzgJgdbNxXvX8= +cuelang.org/go v0.13.2 h1:SagzeEASX4E2FQnRbItsqa33sSelrJjQByLqH9uZCE8= +cuelang.org/go v0.13.2/go.mod h1:8MoQXu+RcXsa2s9mebJN1HJ1orVDc9aI9/yKi6Dzsi4= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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.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/CycloneDX/cyclonedx-go v0.9.1 h1:yffaWOZsv77oTJa/SdVZYdgAgFioCeycBUKkqS2qzQM= +github.com/CycloneDX/cyclonedx-go v0.9.1/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= +github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= +github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/KeisukeYamashita/go-vcl v0.4.0 h1:dFxZq2yVeaCWBJAT7Oh9Z+Pp8y32i7b11QHdzsuBcsk= +github.com/KeisukeYamashita/go-vcl v0.4.0/go.mod h1:af2qGlXbsHDQN5abN7hyGNKtGhcFSaDdbLl4sfud+AU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA= +github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= +github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= +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/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= +github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/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/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +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/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/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665 h1:Iz3aEheYgn+//VX7VisgCmF/wW3BMtXCLbvHV4jMQJA= +github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665/go.mod h1:19bUnum2ZAeftfwwLZ/wRe7idyfoW2MfmXO464Hrfbw= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +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-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +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-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godoctor/godoctor v0.0.0-20181123222458-69df17f3a6f6/go.mod h1:+tyhT8jBF8E0XvdlSXOSL7Iko7DlNiongHq3q+wcsPs= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= +github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= +github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= +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/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/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/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/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= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +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/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.6 h1:5jHuM+aH373XNtXl9TNTUH5Qd69Trve11tHIrB+6yj4= +github.com/hashicorp/go-getter v1.7.6/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= +github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +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.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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds= +github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +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/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +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/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +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.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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/moby/buildkit v0.16.0 h1:wOVBj1o5YNVad/txPQNXUXdelm7Hs/i0PUFjzbK0VKE= +github.com/moby/buildkit v0.16.0/go.mod h1:Xqx/5GlrqE1yIRORk0NSCVDFpQAU1WjlT6KHYZdisIQ= +github.com/moby/buildkit v0.23.2 h1:gt/dkfcpgTXKx+B9I310kV767hhVqTvEyxGgI3mqsGQ= +github.com/moby/buildkit v0.23.2/go.mod h1:iEjAfPQKIuO+8y6OcInInvzqTMiKMbb2RdJz1K/95a0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs= +github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= +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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/open-policy-agent/conftest v0.56.0 h1:Q27Y45rdUHAOTjkeTbmHf2kWgW+DeFauZMaDjJm98YA= +github.com/open-policy-agent/conftest v0.56.0/go.mod h1:u4xu/0jtZnsenKf06J/tdm/7CtP8ODmZ/JsRPTDCXMg= +github.com/open-policy-agent/conftest v0.62.0 h1:mk6Kbf8WTGjI8byKd59GWjIGsOPr+dmiEwjyDEZMWhk= +github.com/open-policy-agent/conftest v0.62.0/go.mod h1:oX2ScMAaFCJ2f4bAy23GBibaUzn1b8lRs6gkhu4G+IA= +github.com/open-policy-agent/opa v0.69.0 h1:s2igLw2Z6IvGWGuXSfugWkVultDMsM9pXiDuMp7ckWw= +github.com/open-policy-agent/opa v0.69.0/go.mod h1:+qyXJGkpEJ6kpB1kGo8JSwHtVXbTdsGdQYPWWNYNj+4= +github.com/open-policy-agent/opa v1.6.0 h1:/S/cnNQJ2MUMNzizHPbisTWBHowmLkPrugY5jjkPlRQ= +github.com/open-policy-agent/opa v1.6.0/go.mod h1:zFmw4P+W62+CWGYRDDswfVYSCnPo6oYaktQnfIaRFC4= +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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +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/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= +github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= +github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +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-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +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/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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shteou/go-ignore v0.3.1 h1:/DVY4w06eKliWrbkwKfBHJgUleld+QAlmlQvfRQOigA= +github.com/shteou/go-ignore v0.3.1/go.mod h1:hMVyBe+qt5/Z11W/Fxxf86b5SuL8kM29xNWLYob9Vos= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= +github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +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/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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 v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +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.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tmccombs/hcl2json v0.3.1 h1:Pf+Lb9OpZ5lkQuIC0BB5txdCQskZ2ud/l8sz/Nkjf3A= +github.com/tmccombs/hcl2json v0.3.1/go.mod h1:ljY0/prd2IFUF3cagQjV3cpPEEQKzqyGqnKI7m5DBVY= +github.com/tmccombs/hcl2json v0.6.7 h1:RYKTs4kd/gzRsEiv7J3M2WQ7TYRYZVc+0H0pZdERkxA= +github.com/tmccombs/hcl2json v0.6.7/go.mod h1:lJgBOOGDpbhjvdG2dLaWsqB4KBzul2HytfDTS3H465o= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= +github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8xLRkY= +github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o= +github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70= +github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +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/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +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.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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.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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.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.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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.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.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180811021610-c39426892332/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +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.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +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.7.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +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.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-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-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/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-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/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.3.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.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.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.29.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/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= +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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +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.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.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/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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/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-20210106214847-113979e3529a/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/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +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.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.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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/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/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY= +google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= +google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= +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= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/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-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +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/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +muzzammil.xyz/jsonc v1.0.0 h1:B6kaT3wHueZ87mPz3q1nFuM1BlL32IG0wcq0/uOsQ18= +muzzammil.xyz/jsonc v1.0.0/go.mod h1:rFv8tUUKe+QLh7v02BhfxXEf4ZHhYD7unR93HL/1Uvo= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +oras.land/oras-go/v2 v2.4.0 h1:i+Wt5oCaMHu99guBD0yuBjdLvX7Lz8ukPbwXdR7uBMs= +oras.land/oras-go/v2 v2.4.0/go.mod h1:osvtg0/ClRq1KkydMAEu/IxFieyjItcsQ4ut4PPF+f8= +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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +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/controller-gen.mod b/.bingo/controller-gen.mod index 898ad469d8..3a1d74ca30 100644 --- a/.bingo/controller-gen.mod +++ b/.bingo/controller-gen.mod @@ -1,5 +1,5 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.24.6 +go 1.25.0 -require sigs.k8s.io/controller-tools v0.19.0 // cmd/controller-gen +require sigs.k8s.io/controller-tools v0.20.0 // cmd/controller-gen diff --git a/.bingo/controller-gen.sum b/.bingo/controller-gen.sum index 864468974c..0c3abcb92b 100644 --- a/.bingo/controller-gen.sum +++ b/.bingo/controller-gen.sum @@ -52,9 +52,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.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/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -68,6 +73,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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/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= @@ -77,17 +84,23 @@ 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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -95,22 +108,30 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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= @@ -122,24 +143,42 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= 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/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 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/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= +k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= 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/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= 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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 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/controller-tools v0.20.0 h1:VWZF71pwSQ2lZZCt7hFGJsOfDc5dVG28/IysjjMWXL8= +sigs.k8s.io/controller-tools v0.20.0/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU= 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/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/.bingo/crd-ref-docs.mod b/.bingo/crd-ref-docs.mod index 1e73dd5905..3c514cb8ab 100644 --- a/.bingo/crd-ref-docs.mod +++ b/.bingo/crd-ref-docs.mod @@ -1,5 +1,5 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.24.0 +go 1.25.0 -require github.com/elastic/crd-ref-docs v0.2.0 +require github.com/elastic/crd-ref-docs v0.3.0 diff --git a/.bingo/crd-ref-docs.sum b/.bingo/crd-ref-docs.sum index b16a8d02fa..9d9c37c7b7 100644 --- a/.bingo/crd-ref-docs.sum +++ b/.bingo/crd-ref-docs.sum @@ -1,9 +1,15 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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 v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +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 v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -12,14 +18,20 @@ github.com/elastic/crd-ref-docs v0.1.0 h1:Cr5kz89QB3Iuuj7dhAfLMApCrChEGAaIBTxGk/ github.com/elastic/crd-ref-docs v0.1.0/go.mod h1:X83mMBdJt05heJUYiS3T0yJ/JkCuliuhSUNav5Gjo/U= github.com/elastic/crd-ref-docs v0.2.0 h1:U17MyGX71j4qfKTvYxbR4qZGoA1hc2thy7kseGYmP+o= github.com/elastic/crd-ref-docs v0.2.0/go.mod h1:0bklkJhTG7nC6AVsdDi0wt5bGoqvzdZSzMMQkilZ6XM= +github.com/elastic/crd-ref-docs v0.3.0 h1:9bGSUkBR56Z7TuDGQAu3KGbBkagwwZ6RkZmS+qvDuDM= +github.com/elastic/crd-ref-docs v0.3.0/go.mod h1:8td3UC8CaO5M+G115O3FRKLmplmX+p0EqLMLGM6uNdk= 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/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 v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/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/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= @@ -28,6 +40,8 @@ github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -40,6 +54,8 @@ 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/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +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/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -62,16 +78,27 @@ 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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.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.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -87,6 +114,11 @@ 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= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.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/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= @@ -94,12 +126,16 @@ golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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= @@ -108,11 +144,15 @@ golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -126,6 +166,8 @@ 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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 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= @@ -134,6 +176,8 @@ 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.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= 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= @@ -153,26 +197,38 @@ k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3 k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= 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.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= 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-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/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-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= +k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= 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.20.0 h1:VWZF71pwSQ2lZZCt7hFGJsOfDc5dVG28/IysjjMWXL8= +sigs.k8s.io/controller-tools v0.20.0/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU= 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-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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= @@ -180,5 +236,7 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 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.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/.bingo/golangci-lint.mod b/.bingo/golangci-lint.mod index 4607edf929..0dcb9837a8 100644 --- a/.bingo/golangci-lint.mod +++ b/.bingo/golangci-lint.mod @@ -2,4 +2,4 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT go 1.24.6 -require github.com/golangci/golangci-lint/v2 v2.6.2 // cmd/golangci-lint +require github.com/golangci/golangci-lint/v2 v2.8.0 // cmd/golangci-lint diff --git a/.bingo/golangci-lint.sum b/.bingo/golangci-lint.sum index 3146c71502..a8c8a0fba6 100644 --- a/.bingo/golangci-lint.sum +++ b/.bingo/golangci-lint.sum @@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= +codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= +codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= @@ -58,6 +60,8 @@ github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= @@ -65,10 +69,16 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MirrexOne/unqueryvet v1.2.1 h1:M+zdXMq84g+E1YOLa7g7ExN3dWfZQrdDSTCM7gC+m/A= github.com/MirrexOne/unqueryvet v1.2.1/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= +github.com/MirrexOne/unqueryvet v1.3.0 h1:5slWSomgqpYU4zFuZ3NNOfOUxVPlXFDBPAVasZOGlAY= +github.com/MirrexOne/unqueryvet v1.3.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= +github.com/MirrexOne/unqueryvet v1.4.0 h1:6KAkqqW2KUnkl9Z0VuTphC3IXRPoFqEkJEtyxxHj5eQ= +github.com/MirrexOne/unqueryvet v1.4.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= +github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -80,6 +90,8 @@ github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQ 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/alexkohler/prealloc v1.0.1 h1:A9P1haqowqUxWvU9nk6tQ7YktXIHf+LQM9wPRhuteEE= +github.com/alexkohler/prealloc v1.0.1/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= @@ -114,6 +126,8 @@ github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/catenacyber/perfsprint v0.10.0 h1:AZj1mYyxbxLRqmnYOeguZXEQwWOgQGm2wzLI5d7Hl/0= github.com/catenacyber/perfsprint v0.10.0/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= +github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= +github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -172,8 +186,12 @@ 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.17 h1:sjGPErP9o7i2Ym+z3LsQzBdLCNaqbYy2iJQPxGXg04Q= github.com/ghostiam/protogetter v0.3.17/go.mod h1:AivIX1eKA/TcUmzZdzbl+Tb8tjIe8FcyG6JFyemQAH4= +github.com/ghostiam/protogetter v0.3.18 h1:yEpghRGtP9PjKvVXtEzGpYfQj1Wl/ZehAfU6fr62Lfo= +github.com/ghostiam/protogetter v0.3.18/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo= github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -209,6 +227,10 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godoc-lint/godoc-lint v0.10.1 h1:ZPUVzlDtJfA+P688JfPJPkI/SuzcBr/753yGIk5bOPA= github.com/godoc-lint/godoc-lint v0.10.1/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw= +github.com/godoc-lint/godoc-lint v0.10.2 h1:dksNgK+zebnVlj4Fx83CRnCmPO0qRat/9xfFsir1nfg= +github.com/godoc-lint/godoc-lint v0.10.2/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw= +github.com/godoc-lint/godoc-lint v0.11.1 h1:z9as8Qjiy6miRIa3VRymTa+Gt2RLnGICVikcvlUVOaA= +github.com/godoc-lint/godoc-lint v0.11.1/go.mod h1:BAqayheFSuZrEAqCRxgw9MyvsM+S/hZwJbU1s/ejRj8= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -251,8 +273,14 @@ github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0a github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= github.com/golangci/golangci-lint/v2 v2.6.2 h1:jkMSVv36JmyTENcEertckvimvjPcD5qxNM7W7qhECvI= github.com/golangci/golangci-lint/v2 v2.6.2/go.mod h1:fSIMDiBt9kzdpnvvV7GO6iWzyv5uaeZ+iPor+2uRczE= +github.com/golangci/golangci-lint/v2 v2.7.2 h1:AhBC+YeEueec4AGlIbvPym5C70Thx0JykIqXbdIXWx0= +github.com/golangci/golangci-lint/v2 v2.7.2/go.mod h1:pDijleoBu7e8sejMqyZ3L5n6geqe+cVvOAz2QImqqVc= +github.com/golangci/golangci-lint/v2 v2.8.0 h1:wJnr3hJWY3eVzOUcfwbDc2qbi2RDEpvLmQeNFaPSNYA= +github.com/golangci/golangci-lint/v2 v2.8.0/go.mod h1:xl+HafQ9xoP8rzw0z5AwnO5kynxtb80e8u02Ej/47RI= github.com/golangci/golines v0.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/golines v0.14.0 h1:xt9d3RKBjhasA3qpoXs99J2xN2t6eBlpLHt0TrgyyXc= +github.com/golangci/golines v0.14.0/go.mod h1:gf555vPG2Ia7mmy2mzmhVQbVjuK8Orw0maR1G4vVAAQ= github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c= github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= @@ -308,6 +336,8 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T github.com/hashicorp/go-version v1.2.1/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/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -359,8 +389,12 @@ github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= github.com/ldez/gomoddirectives v0.7.1 h1:FaULkvUIG36hj6chpwa+FdCNGZBsD7/fO+p7CCsM6pE= github.com/ldez/gomoddirectives v0.7.1/go.mod h1:auDNtakWJR1rC+YX7ar+HmveqXATBAyEK1KYpsIRW/8= +github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= +github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= +github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= +github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= @@ -394,6 +428,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU= github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8= +github.com/mgechev/revive v1.13.0 h1:yFbEVliCVKRXY8UgwEO7EOYNopvjb1BFbmYqm9hZjBM= +github.com/mgechev/revive v1.13.0/go.mod h1:efJfeBVCX2JUumNQ7dtOLDja+QKj9mYGgEZA7rt5u+0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -488,6 +524,10 @@ github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iM github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= github.com/securego/gosec/v2 v2.22.10 h1:ntbBqdWXnu46DUOXn+R2SvPo3PiJCDugTCgTW2g4tQg= github.com/securego/gosec/v2 v2.22.10/go.mod h1:9UNjK3tLpv/w2b0+7r82byV43wCJDNtEDQMeS+H/g2w= +github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7 h1:rZg6IGn0ySYZwCX8LHwZoYm03JhG/cVAJJ3O+u3Vclo= +github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7/go.mod h1:9sr22NZO5Kfh7unW/xZxkGYTmj2484/fCiE54gw7UTY= +github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg= +github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -503,10 +543,14 @@ github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCp github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= 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/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -519,6 +563,8 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= 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/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= +github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -541,6 +587,8 @@ github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwT 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/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= +github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= @@ -589,6 +637,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -642,6 +692,10 @@ 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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -703,6 +757,8 @@ 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -756,6 +812,10 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -774,6 +834,8 @@ 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -829,6 +891,10 @@ 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/.bingo/goreleaser.mod b/.bingo/goreleaser.mod index d4e6c38326..3fe6a44059 100644 --- a/.bingo/goreleaser.mod +++ b/.bingo/goreleaser.mod @@ -1,5 +1,5 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT -go 1.22.5 +go 1.24.6 -require github.com/goreleaser/goreleaser v1.26.2 +require github.com/goreleaser/goreleaser/v2 v2.11.2 diff --git a/.bingo/goreleaser.sum b/.bingo/goreleaser.sum index c5a6760d46..7d1df8e6fd 100644 --- a/.bingo/goreleaser.sum +++ b/.bingo/goreleaser.sum @@ -1,199 +1,239 @@ +al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= +al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +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= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= -cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= -cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= -cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= -cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY= -cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o= -code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI= -code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= +code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= +github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2htVQTBY8nOZpyajYztF0vUvSZTuM= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1 h1:fXPMAmuh0gDuRDey0atC8cXBuKIlqCzCkL8sm1n9Ov0= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1/go.mod h1:SUZc9YRRHfx2+FAQKNDGrssXehqLpxmwRv2mC/5ntj4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= -github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= -github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc= +github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= 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.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -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/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/anchore/bubbly v0.0.0-20230518153401-87b6af8ccf22 h1:5NFK6VGgqBUOAX2SYyzFYvNdOiYDxzim8jga386FlZY= -github.com/anchore/bubbly v0.0.0-20230518153401-87b6af8ccf22/go.mod h1:Kv+Mm9CdtnV8iem48iEPIwy7/N4Wmk0hpxYNH5gTwKQ= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= -github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/anchore/bubbly v0.0.0-20241107060245-f2a5536f366a h1:smr1CcMkgeMd6G75N+2OVNk/uHbX/WLR0bk+kMWEyr8= +github.com/anchore/bubbly v0.0.0-20241107060245-f2a5536f366a/go.mod h1:P5IrP8AhuzApVKa5H7k2hHX5pZA1uhyi+Z1VjK1EtA4= +github.com/anchore/go-logger v0.0.0-20241005132348-65b4486fbb28 h1:TKlTOayTJKpoLPJbeMykEwxCn0enACf06u0RSIdFG5w= +github.com/anchore/go-logger v0.0.0-20241005132348-65b4486fbb28/go.mod h1:5iJIa34inbIEFRwoWxNBTnjzIcl4G3le1LppPDmpg/4= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= -github.com/anchore/quill v0.4.1 h1:mffDnvnER3ZgPjN5hexc3nr/4Y1dtKdDB6td5K8uInk= -github.com/anchore/quill v0.4.1/go.mod h1:t6hOPYDohN8wn2SRWQdNkJBkhmK8s3gzuHzzgcEvzQU= +github.com/anchore/quill v0.5.1 h1:+TAJroWuMC0AofI4gD9V9v65zR8EfKZg8u+ZD+dKZS4= +github.com/anchore/quill v0.5.1/go.mod h1:tAzfFxVluL2P1cT+xEy+RgQX1hpNuliUC5dTYSsnCLQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 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/atc0005/go-teams-notify/v2 v2.10.0 h1:eQvRIkyESQgBvlUdQ/iPol/lj3QcRyrdEQM3+c/nXhM= -github.com/atc0005/go-teams-notify/v2 v2.10.0/go.mod h1:SIeE1UfCcVRYMqP5b+r1ZteHyA/2UAjzWF5COnZ8q0w= -github.com/aws/aws-sdk-go v1.53.0 h1:MMo1x1ggPPxDfHMXJnQudTbGXYlD4UigUAud1DJxPVo= -github.com/aws/aws-sdk-go v1.53.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= -github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo= -github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A= -github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9 h1:vXY/Hq1XdxHBIYgBUmug/AbMyIe1AKulPYS2/VE1X70= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9/go.mod h1:GyJJTZoHVuENM4TeJEl5Ffs4W9m19u+4wKJcDi/GZ4A= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.3 h1:mDnFOE2sVkyphMWtTH+stv0eW3k0OTx94K63xpxHty4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.3/go.mod h1:V8MuRVcCRt5h1S+Fwu8KbC7l/gBGo3yBAyUbJM2IJOk= -github.com/aws/aws-sdk-go-v2/service/ecr v1.28.0 h1:rdPrcOZmqT2F+yzmKEImrx5XUs7Hpf4V9Rp6E8mhsxQ= -github.com/aws/aws-sdk-go-v2/service/ecr v1.28.0/go.mod h1:if7ybzzjOmDB8pat9FE35AHTY6ZxlYSy3YviSmFZv8c= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.5 h1:452e/nFuqPvwPg+1OD2CG/v29R9MH8egJSJKh2Qduv8= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.5/go.mod h1:8pvvNAklmq+hKmqyvFoMRg0bwg9sdGOvdwximmKiKP0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.5 h1:mbWNpfRUTT6bnacmvOTKXZjR/HycibdWzNpfbrbLDIs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.5/go.mod h1:FCOPWGjsshkkICJIn9hq9xr6dLKtyaWpuUojiN3W1/8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.3 h1:4t+QEX7BsXz98W8W1lNvMAG+NX8qHz2CjLBxQKku40g= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.3/go.mod h1:oFcjjUq5Hm09N9rpxTdeMeLeQcxS7mIkBkL8qUKng+A= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0 h1:yS0JkEdV6h9JOo8sy2JSpjX+i7vsKifU8SIeHrqiDhU= -github.com/aws/aws-sdk-go-v2/service/kms v1.30.0/go.mod h1:+I8VUUSVD4p5ISQtzpgSva4I8cJ4SQ4b1dcBcof7O+g= -github.com/aws/aws-sdk-go-v2/service/s3 v1.51.4 h1:lW5xUzOPGAMY7HPuNF4FdyBwRc3UJ/e8KsapbesVeNU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.51.4/go.mod h1:MGTaf3x/+z7ZGugCGvepnx2DS6+caCYYqKhzVoLNYPk= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240514230400-03fa26f5508f h1:Z0kS9pJDQgCg3u2lH6+CdYaFbyQtyukVTiUCG6re0E4= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20240514230400-03fa26f5508f/go.mod h1:rAE739ssmE5O5fLuQ2y8uHdmOJaelE5I0Es3SxV0y1A= +github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw= +github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.37.1 h1:SMUxeNz3Z6nqGsXv0JuJXc8w5YMtrQMuIBmDx//bBDY= +github.com/aws/aws-sdk-go-v2 v1.37.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= +github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69 h1:6VFPH/Zi9xYFMJKPQOX5URYkQoXRWeJ7V/7Y6ZDYoms= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.1 h1:ksZXBYv80EFTcgc8OJO48aQ8XDWXIQL7gGasPeCoTzI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.1/go.mod h1:HSksQyyJETVZS7uM54cir0IgxttTD+8aEoJMPGepHBI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.1 h1:+dn/xF/05utS7tUhjIcndbuaPjfll2LhbH1cCDGLYUQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.1/go.mod h1:hyAGz30LHdm5KBZDI58MXx5lDVZ5CUfvfTZvMu4HCZo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.1 h1:4HbnOGE9491a9zYJ9VpPh1ApgEq6ZlD4Kuv1PJenFpc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.1/go.mod h1:Z6QnHC6TmpJWUxAy8FI4JzA7rTwl6EIANkyK9OR5z5w= +github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1 h1:Bwzh202Aq7/MYnAjXA9VawCf6u+hjwMdoYmZ4HYsdf8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1/go.mod h1:xZzWl9AXYa6zsLLH41HBFW8KRKJRIzlGmvSM0mVMIX4= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2 h1:XJ/AEFYj9VFPJdF+VFi4SUPEDfz1akHwxxm07JfZJcs= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2/go.mod h1:JUBHdhvKbbKmhaHjLsKJAWnQL80T6nURmhB/LEprV+4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.1 h1:ps3nrmBWdWwakZBydGX1CxeYFK80HsQ79JLMwm7Y4/c= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.1/go.mod h1:bAdfrfxENre68Hh2swNaGEVuFYE74o0SaSCAlaG9E74= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.1 h1:ky79ysLMxhwk5rxJtS+ILd3Mc8kC5fhsLBrP27r6h4I= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.1/go.mod h1:+2MmkvFvPYM1vsozBWduoLJUi5maxFk5B7KJFECujhY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.1 h1:MdVYlN5pcQu1t1OYx4Ajo3fKl1IEhzgdPQbYFCRjYS8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.1/go.mod h1:iikmNLrvHm2p4a3/4BPeix2S9P+nW8yM1IZW73x8bFA= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.85.1 h1:Hsqo8+dFxSdDvv9B2PgIx1AJAnDpqgS0znVI+R+MoGY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.85.1/go.mod h1:8Q0TAPXD68Z8YqlcIGHs/UNIDHsxErV9H4dl4vJEpgw= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.10.1 h1:6lMw4/QGLFPvbKQ0eri/9Oh3YX5Nm6BPrUlZR8yuJHg= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.10.1/go.mod h1:EVJOSYOVeoD3VFFZ/dWCAzWJp5wZr9lTOCjW8ejAmO0= 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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/blacktop/go-dwarf v1.0.9 h1:eT/L7gt0gllvvgnRXY0MFKjNB6+jtOY5DTm2ynVX2dY= -github.com/blacktop/go-dwarf v1.0.9/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= -github.com/blacktop/go-macho v1.1.162 h1:FjM3XAsJTAOGZ1eppRSX9ZBX3Bk11JMTC1amsZAOA5I= -github.com/blacktop/go-macho v1.1.162/go.mod h1:f2X4noFBob4G5bWUrzvPBKDVcFWZgDCM7rIn7ygTID0= +github.com/blacktop/go-dwarf v1.0.10 h1:i9zYgcIROETsNZ6V+zZn3uDH21FCG5BLLZ837GitxS0= +github.com/blacktop/go-dwarf v1.0.10/go.mod h1:4W2FKgSFYcZLDwnR7k+apv5i3nrau4NGl9N6VQ9DSTo= +github.com/blacktop/go-macho v1.1.238 h1:OFfT6NB/SWxkoky7L/ytuY8QekgFpa9pmz/GHUQLsmM= +github.com/blacktop/go-macho v1.1.238/go.mod h1:dtlW2AJKQpFzImBVPWiUKZ6OxrQ2MLfWi/BPPe0EONE= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/bluesky-social/indigo v0.0.0-20240411170459-440932307e0d h1:xxPhzCOpmOntzVe8S6tqsMdFgaB8B4NXSV54lG4B1qk= -github.com/bluesky-social/indigo v0.0.0-20240411170459-440932307e0d/go.mod h1:ysMQ0a4RYWjgyvKrl5ME352oHA6QgK900g5sB9XXgPE= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043 h1:927VIkxPFKpfJKVDtCNgSQtlhksARaLvsLxppR2FukM= +github.com/bluesky-social/indigo v0.0.0-20240813042137-4006c0eca043/go.mod h1:dXjdzg6bhg1JKnKuf6EBJTtcxtfHYBFEe9btxX5YeAE= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/caarlos0/ctrlc v1.2.0 h1:AtbThhmbeYx1WW3WXdWrd94EHKi+0NPRGS4/4pzrjwk= -github.com/caarlos0/ctrlc v1.2.0/go.mod h1:n3gDlSjsXZ7rbD9/RprIR040b7oaLfNStikPd4gFago= -github.com/caarlos0/env/v11 v11.0.1 h1:A8dDt9Ub9ybqRSUF3fQc/TA/gTam2bKT4Pit+cwrsPs= -github.com/caarlos0/env/v11 v11.0.1/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/caarlos0/go-reddit/v3 v3.0.1 h1:w8ugvsrHhaE/m4ez0BO/sTBOBWI9WZTjG7VTecHnql4= github.com/caarlos0/go-reddit/v3 v3.0.1/go.mod h1:QlwgmG5SAqxMeQvg/A2dD1x9cIZCO56BMnMdjXLoisI= github.com/caarlos0/go-shellwords v1.0.12 h1:HWrUnu6lGbWfrDcFiHcZiwOLzHWjjrPVehULaTFgPp8= github.com/caarlos0/go-shellwords v1.0.12/go.mod h1:bYeeX1GrTLPl5cAMYEzdm272qdsQAZiaHgeF0KTk1Gw= -github.com/caarlos0/go-version v0.1.1 h1:1bikKHkGGVIIxqCmufhSSs3hpBScgHGacrvsi8FuIfc= -github.com/caarlos0/go-version v0.1.1/go.mod h1:Ze5Qx4TsBBi5FyrSKVg1Ibc44KGV/llAaKGp86oTwZ0= -github.com/caarlos0/log v0.4.4 h1:LnvgBz/ofsJ00AupP/cEfksJSZglb1L69g4Obk/sdAc= -github.com/caarlos0/log v0.4.4/go.mod h1:+AmCI9Liv5LKXmzFmFI1htuHdTTj/0R3KuoP9DMY7Mo= +github.com/caarlos0/go-version v0.2.1 h1:bJY5WRvs2RXErLKBELd1WR0U72whX8ELbKg0WtQ9/7A= +github.com/caarlos0/go-version v0.2.1/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944= +github.com/caarlos0/log v0.5.1 h1:uB1jhC/+HimtyyL7pxidkUWO4raKmidVuXifC4uqMf8= +github.com/caarlos0/log v0.5.1/go.mod h1:37k7VCogxsMsgpIQaca5g9eXFFrLJ5LGgA4Ng/xN85o= github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= -github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d h1:+o+e/8hf7cG0SbAzEAm/usJ8qoZPgFXhudLjop+TM0g= -github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d/go.mod h1:aoG4bThKYIOnyB55r202eHqo6TkN7ZXV+cu4Do3eoBQ= +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/charmbracelet/bubbletea v1.3.0 h1:fPMyirm0u3Fou+flch7hlJN9krlnVURrkUVDwqXjoAc= +github.com/charmbracelet/bubbletea v1.3.0/go.mod h1:eTaHfqbIwvBhFQM/nlT1NsGc4kp8jhF8LfUK67XiTDM= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/fang v0.3.0 h1:Be6TB+ExS8VWizTQRJgjqbJBudKrmVUet65xmFPGhaA= +github.com/charmbracelet/fang v0.3.0/go.mod h1:b0ZfEXZeBds0I27/wnTfnv2UVigFDXHhrFNwQztfA0M= +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/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU= +github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= +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 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= -github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= -github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -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/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +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= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +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/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= @@ -204,62 +244,75 @@ github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY= github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v25.0.4+incompatible h1:DatRkJ+nrFoYL2HZUzjM5Z5sAmcA5XGp+AW0oEw2+cA= -github.com/docker/cli v25.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +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.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+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 v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= -github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= -github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= -github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +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/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= 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.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +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.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -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.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-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.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -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/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.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 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= @@ -270,15 +323,16 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z 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.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= @@ -286,14 +340,15 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -304,8 +359,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= 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= @@ -313,73 +368,69 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= -github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= -github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= -github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +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/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= +github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/ko v0.15.4 h1:0blRbIdPmSy6v4LvedGxbI/8krdJYQgbSih3v6Y8V1c= -github.com/google/ko v0.15.4/go.mod h1:ZkcmfV91Xt6ZzOBHc/cXXGYnqWdNWDVy/gHoUU9sjag= +github.com/google/ko v0.18.0 h1:jkF5Fkvm+SMtqTt/SMzsCJO+6hz7FSDE6GRldGn0VVI= +github.com/google/ko v0.18.0/go.mod h1:iR0zT5aR4pINW9tk2Ujj99dBJ7cVy4to9ZirAkGKb9g= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a h1:JJBdjSfqSy3mnDT0940ASQFghwcZ4y4cb6ttjAoXqwE= -github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= +github.com/google/rpmpack v0.7.0 h1:mA2Yd3/dOmao1ype0DJA8DFquEpslaleywOuglVCrUs= +github.com/google/rpmpack v0.7.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 h1:+9C/TgFfcCmZBV7Fjb3kQCGlkpFrhtvFDgbdQHB9RaA= +github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962/go.mod h1:H3K1Iu/utuCfa10JO+GsmKUYSWi7ug57Rk6GaDRHaaQ= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/goreleaser/chglog v0.6.1 h1:NZKiX8l0FTQPRzBgKST7knvNZmZ04f7PEGkN2wInfhE= -github.com/goreleaser/chglog v0.6.1/go.mod h1:Bnnfo07jMZkaAb0uRNASMZyOsX6ROW6X1qbXqN3guUo= +github.com/goreleaser/chglog v0.7.0 h1:/KzXWAeg4DrEz4r3OI6K2Yb8RAsVGeInCUfLWFXL9C0= +github.com/goreleaser/chglog v0.7.0/go.mod h1:2h/yyq9xvTUeM9tOoucBP+jri8Dj28splx+SjlYkklc= github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I= github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU= -github.com/goreleaser/goreleaser v1.26.2 h1:1iY1HaXtRiMTrwy6KE1sNjkRjsjMi+9l0k6WUX8GpWw= -github.com/goreleaser/goreleaser v1.26.2/go.mod h1:mHi6zr6fuuOh5eHdWWgyo/N8BWED5WEVtb/4GETc9jQ= -github.com/goreleaser/nfpm/v2 v2.37.1 h1:RUmeEt8OlEVeSzKRrO5Vl5qVWCtUwx4j9uivGuRo5fw= -github.com/goreleaser/nfpm/v2 v2.37.1/go.mod h1:q8+sZXFqn106/eGw+9V+I8+izFxZ/sJjrhwmEUxXhUg= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/goreleaser/goreleaser/v2 v2.11.2 h1:Od6dcPI5r8IWVPnJYz6wYe3rML1qf80fLzXB1Ix6ZnY= +github.com/goreleaser/goreleaser/v2 v2.11.2/go.mod h1:NSsia+m49thkd/pX9Rz7Cq1KE8HDGrLJVoPLjFeAV/4= +github.com/goreleaser/nfpm/v2 v2.43.0 h1:o5oureuZkhu55RK0M9WSN8JLW7hu6MymtMh7LypInlk= +github.com/goreleaser/nfpm/v2 v2.43.0/go.mod h1:f//PE8PjNHjaPCbd7Jkok+aPKdLTrzM+fuIWg3PfVRg= +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/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-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 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-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= -github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -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-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +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 v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= -github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +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/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI= +github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys= +github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= -github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= @@ -410,9 +461,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic= +github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 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= @@ -421,8 +473,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 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.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +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/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -432,46 +484,40 @@ 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 h1:WGrKdjHtWC67RX96eTkYD2f53NDHhrq/7robWTAfk4s= -github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491/go.mod h1:o158RFmdEbYyIZmXAbrvmJWesbyxlLKee6X64VPVuOc= +github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 h1:ndfLOJNaxu0fX358UKxtq2bU8IMASWi87Hn0Nv/TIoY= +github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666/go.mod h1:WGXwLq/jKt0kng727wv6a0h0q7TVC+MwS2S75rcqL+4= 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= +github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-mastodon v0.0.8 h1:UgKs4SmQ5JeawxMIPP7NQ9xncmOXA+5q6jYk4erR7vk= -github.com/mattn/go-mastodon v0.0.8/go.mod h1:8YkqetHoAVEktRkK15qeiv/aaIMfJ/Gc89etisPZtHU= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -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/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-mastodon v0.0.10 h1:wz1d/aCkJOIkz46iv4eAqXHVreUMxydY1xBWrPBdDeE= +github.com/mattn/go-mastodon v0.0.10/go.mod h1:YBofeqh7G6s787787NQR8erBYz6fKDu+KNMrn5RuD6Y= +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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 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-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -480,13 +526,10 @@ github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbY github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +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/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= @@ -501,27 +544,30 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +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/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +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/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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= @@ -529,113 +575,143 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= -github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= -github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= +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/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sigstore/cosign/v2 v2.2.4 h1:iY4vtEacmu2hkNj1Fh+8EBqBwKs2DHM27/lbNWDFJro= -github.com/sigstore/cosign/v2 v2.2.4/go.mod h1:JZlRD2uaEjVAvZ1XJ3QkkZJhTqSDVtLaet+C/TMR81Y= -github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= -github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4= -github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs= +github.com/sigstore/cosign/v2 v2.5.0 h1:1aRfPgRQHHlODI3Mvs/JkPBS9dJT9bRLCuHZgnHxFt8= +github.com/sigstore/cosign/v2 v2.5.0/go.mod h1:2V2hmo+jjFNnDb5Q5VL6PXvLU9Vujio7T5yldrpNTRw= +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.9 h1:sUjRpKVh/hhgqGMs0t+TubgYsksArZ6poLEC3MsGAzU= +github.com/sigstore/rekor v1.3.9/go.mod h1:xThNUhm6eNEmkJ/SiU/FVU7pLY2f380fSDZFsdDWlcM= +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/sigstore/sigstore-go v0.7.1 h1:lyzi3AjO6+BHc5zCf9fniycqPYOt3RaC08M/FRmQhVY= +github.com/sigstore/sigstore-go v0.7.1/go.mod h1:AIRj4I3LC82qd07VFm3T2zXYiddxeBV1k/eoS8nTz0E= +github.com/sigstore/timestamp-authority v1.2.5 h1:W22JmwRv1Salr/NFFuP7iJuhytcZszQjldoB8GiEdnw= +github.com/sigstore/timestamp-authority v1.2.5/go.mod h1:gWPKWq4HMWgPCETre0AakgBzcr9DRqHrsgbrRqsigOs= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/slack-go/slack v0.13.0 h1:7my/pR2ubZJ9912p9FtvALYpbt0cQPAqkRy2jaSI1PQ= -github.com/slack-go/slack v0.13.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= +github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -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= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +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.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/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/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.0.2 h1:PyNnjV9BJNzN1ZE6BcWK+5JbF+if370jjzO84SS+Ebo= +github.com/theupdateframework/go-tuf/v2 v2.0.2/go.mod h1:baB22nBHeHBCeuGZcIlctNq4P61PcOdyARlplg5xmLA= 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/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +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/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c h1:gFwUKtkv6QzQsFdIjvPqd0Qdw42DHUEbbUdiUTI1uco= github.com/wagoodman/go-progress v0.0.0-20220614130704-4b1c25a33c7c/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= -github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302 h1:MhInbXe4SzcImAKktUvWBCWZgcw6MYf5NfumTj1BhAw= -github.com/whyrusleeping/cbor-gen v0.1.1-0.20240311221002-68b9f235c302/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/whyrusleeping/cbor-gen v0.1.3-0.20240731173018-74d74643234c h1:Jmc9fHbd0LKFmS5CkLgczNUyW36UbiyvbHCG9xCTyiw= +github.com/whyrusleeping/cbor-gen v0.1.3-0.20240731173018-74d74643234c/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/xanzy/go-gitlab v0.105.0 h1:3nyLq0ESez0crcaM19o5S//SvezOQguuIHZ3wgX64hM= -github.com/xanzy/go-gitlab v0.105.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= -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= +gitlab.com/gitlab-org/api/client-go v0.137.0 h1:H26yL44qnb38Czl20pEINCJrcj63W6/BX8iKPVUKQP0= +gitlab.com/gitlab-org/api/client-go v0.137.0/go.mod h1:AcAYES3lfkIS4zhso04S/wyUaWQmDYve2Fd9AF7C6qc= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +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/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +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.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -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/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -646,8 +722,8 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro= -gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco= +gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw= +gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -655,20 +731,16 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 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= @@ -681,8 +753,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 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.14.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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -696,17 +768,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 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= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -717,8 +787,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ 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 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20180830151530-49385e6e1522/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= @@ -731,45 +801,40 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.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.3.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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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.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.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/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 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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= @@ -787,34 +852,34 @@ 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.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 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= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk= -google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= 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-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= -google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U= -google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7 h1:oqta3O3AnlWbmIE3bFnWbu4bRxZjfbWCp0cKSuZh01E= -google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/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.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 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= @@ -824,18 +889,14 @@ 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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= -gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -843,7 +904,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -852,11 +912,13 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sigs.k8s.io/kind v0.23.0 h1:8fyDGWbWTeCcCTwA04v4Nfr45KKxbSPH1WO9K+jVrBg= -sigs.k8s.io/kind v0.23.0/go.mod h1:ZQ1iZuJLh3T+O8fzhdi3VWcFTzsdXtNv2ppsHc8JQ7s= +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/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= -software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/.bingo/kind.mod b/.bingo/kind.mod index eece79efb4..589ee9e908 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.24.6 -require sigs.k8s.io/kind v0.30.0 +require sigs.k8s.io/kind v0.31.0 diff --git a/.bingo/kind.sum b/.bingo/kind.sum index 699cf969bc..fadffa939b 100644 --- a/.bingo/kind.sum +++ b/.bingo/kind.sum @@ -29,5 +29,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/kind v0.31.0 h1:UcT4nzm+YM7YEbqiAKECk+b6dsvc/HRZZu9U0FolL1g= +sigs.k8s.io/kind v0.31.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/.bingo/kube-score.mod b/.bingo/kube-score.mod new file mode 100644 index 0000000000..873a8ecb7a --- /dev/null +++ b/.bingo/kube-score.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.24.6 + +require github.com/zegl/kube-score v1.20.0 // cmd/kube-score diff --git a/.bingo/kube-score.sum b/.bingo/kube-score.sum new file mode 100644 index 0000000000..9a4cabc8e4 --- /dev/null +++ b/.bingo/kube-score.sum @@ -0,0 +1,98 @@ +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/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb h1:ioQwBmKdOCpMVS/bDaESqNWXIE/aw4+gsVtysCGMWZ4= +github.com/eidolon/wordwrap v0.0.0-20161011182207-e0f54129b8bb/go.mod h1:ZAPs+OyRzeVJFGvXVDVffgCzQfjg3qU9Ig8G/MU3zZ4= +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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/jstemmer/go-junit-report/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc= +github.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ= +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/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.16/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/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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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= +github.com/zegl/kube-score v1.20.0 h1:J1VqK86SunV4Gg8emPTmwUVxe0rmXnAs5K9ZUbGMKR8= +github.com/zegl/kube-score v1.20.0/go.mod h1:mBOw3S3g7TBG/GziT8xNG15dCFn54/jUeEHndxLinE8= +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/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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +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/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/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/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 fc3a980e0b..7acfcea8d5 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -10,21 +10,25 @@ fi BINGO="${GOBIN}/bingo-v0.9.0" -CONTROLLER_GEN="${GOBIN}/controller-gen-v0.19.0" +CONFTEST="${GOBIN}/conftest-v0.62.0" + +CONTROLLER_GEN="${GOBIN}/controller-gen-v0.20.0" CRD_DIFF="${GOBIN}/crd-diff-v0.5.0" -CRD_REF_DOCS="${GOBIN}/crd-ref-docs-v0.2.0" +CRD_REF_DOCS="${GOBIN}/crd-ref-docs-v0.3.0" GOJQ="${GOBIN}/gojq-v0.12.17" -GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.6.2" +GOLANGCI_LINT="${GOBIN}/golangci-lint-v2.8.0" -GORELEASER="${GOBIN}/goreleaser-v1.26.2" +GORELEASER="${GOBIN}/goreleaser-v2.11.2" HELM="${GOBIN}/helm-v3.18.4" -KIND="${GOBIN}/kind-v0.30.0" +KIND="${GOBIN}/kind-v0.31.0" + +KUBE_SCORE="${GOBIN}/kube-score-v1.20.0" KUSTOMIZE="${GOBIN}/kustomize-v5.7.1" diff --git a/.claude/commands/api-lint-diff.md b/.claude/commands/api-lint-diff.md new file mode 100644 index 0000000000..549809bc42 --- /dev/null +++ b/.claude/commands/api-lint-diff.md @@ -0,0 +1,200 @@ +--- +description: Validate API issues using kube-api-linter with diff-aware analysis +--- + +# API Lint Diff + +Validates API issues in `api/` directory using kube-api-linter with diff-aware analysis that distinguishes between FIXED, NEW, and PRE-EXISTING issues. + +## Instructions for Claude AI + +When this command is invoked, you MUST: + +**CRITICAL:** The final output MUST be a comprehensive analysis report displayed directly in the conversation for the user to read. Do NOT just create temp files - output the full report as your response. + +1. **Execute the shell script** + ```bash + bash hack/api-lint-diff/run.sh + ``` + +2. **Understand the shell script's output**: + - **False positives (IGNORED)**: Standard CRD scaffolding patterns that kube-api-linter incorrectly flags + - **FIXED issues (SUCCESS)**: Issues that existed in baseline but were resolved in current branch → Celebrate! 🎉 + - **NEW issues (ERRORS)**: Introduced in current branch → MUST fix + - **PRE-EXISTING issues (WARNINGS)**: Existed before changes → Can fix separately + +3. **Filter false positives** - Operator projects scaffold standard Kubernetes CRD patterns that kube-api-linter incorrectly flags as errors, even with WhenRequired configuration. + + **Scenario 1: optionalfields on Status field** + ```go + Status MyResourceStatus `json:"status,omitzero"` + ``` + **Error reported:** + ``` + optionalfields: field Status has a valid zero value ({}), but the validation + is not complete (e.g. min properties/adding required fields). The field should + be a pointer to allow the zero value to be set. If the zero value is not a + valid use case, complete the validation and remove the pointer. + ``` + **Why it's a FALSE POSITIVE:** + - Status is NEVER a pointer in any Kubernetes API + - Works correctly with `omitzero` tag + - Validation incompleteness is expected - Status is controller-managed, not user-provided + - **ACTION: IGNORE this error** + + **Scenario 2: nonpointerstructs on Spec field** + ```go + Spec MyResourceSpec `json:"spec"` + ``` + **Error reported:** + ``` + requiredfields: field Spec has a valid zero value ({}), but the validation is + not complete (e.g. min properties/adding required fields). The field should be + a pointer to allow the zero value to be set. If the zero value is not a valid + use case, complete the validation and remove the pointer. + ``` + **Why it's a FALSE POSITIVE:** + - Spec is NEVER a pointer in Kubernetes APIs + - Scaffolds are starting points - users add validation when they implement their business logic + - **ACTION: IGNORE this error** + + **Scenario 3: conditions markers on metav1.Condition** + ```go + Conditions []metav1.Condition `json:"conditions,omitempty"` + ``` + **Error reported:** + ``` + conditions: Conditions field is missing the following markers: + patchStrategy=merge, patchMergeKey=type + ``` + **Why it's a FALSE POSITIVE:** + - `metav1.Condition` already handles patches correctly + - Adding these markers is redundant for this standard Kubernetes type + - **ACTION: IGNORE this error** + +4. **For reported issues, provide intelligent analysis**: + + a. **Categorize by fix complexity**: + - NON-BREAKING: Marker replacements, adding listType, adding +required/+optional + - BREAKING: Pointer conversions, type changes (int→int32) + + b. **Search for actual usage** (REQUIRED FOR ALL ISSUES - NOT OPTIONAL): + - **CRITICAL:** Do NOT just look at JSON tags - analyze actual code usage patterns + - **Exception:** Deprecated marker replacements (`+kubebuilder:validation:Required` → `+required`) are straightforward - no usage analysis needed + - **For all other issues:** MUST analyze actual usage before making recommendations + - Use grep to find ALL occurrences where each field is: + * **Read/accessed**: `obj.Spec.FieldName`, `cat.Spec.Priority` + * **Written/set**: `obj.Spec.FieldName = value` + * **Checked for zero/nil**: `if obj.Spec.FieldName == ""`, `if ptr != nil` + * **Used in conditionals**: Understand semantic meaning of zero values + - Search in: controllers, reconcilers, internal packages, tests, examples + - **Smart assessment based on usage patterns**: + * Field ALWAYS set in code? → Should be **required**, no omitempty + * Field SOMETIMES set? → Should be **optional** with omitempty + * Code checks `if field == zero`? → May need **pointer** to distinguish zero vs unset + * Zero value semantically valid? → Keep as value type with omitempty + * Zero value semantically invalid? → Use pointer OR mark required + * Field never read but only set by controller? → Likely Status field + - **Example analysis workflow for a field**: + ``` + 1. Grep for field usage: `CatalogFilter.Version` + 2. Found 5 occurrences: + - controllers/extension.go:123: if filter.Version != "" { ... } + - controllers/extension.go:456: result.Version = bundle.Version + - tests/filter_test.go:89: Version: "1.2.3" + 3. Analysis: Version is checked for empty, sometimes set, sometimes omitted + 4. Recommendation: Optional with omitempty (current usage supports this) + ``` + + c. **Generate EXACT code fixes** grouped by file: + - Show current code + - Show replacement code, ready to copy and paste + - **Explain why based on actual usage analysis** (not just JSON tags): + * Include usage summary: "Found N occurrences" + * Cite specific examples: "Used in resolve/catalog.go:163 as direct int32" + * Explain semantic meaning: "Field distinguishes priority 0 vs unset" + * Justify recommendation: "Since code checks for empty, should be optional" + - Note breaking change impact with reasoning + - **Each fix MUST include evidence from code usage** + + d. **Prioritize recommendations**: + - NEW issues first (must fix) + - Group PRE-EXISTING by NON-BREAKING vs BREAKING + +5. **Present actionable report directly to user**: + - **IMPORTANT:** Output the full comprehensive analysis in the conversation (not just to a temp file) + - Summary: False positives filtered, NEW count, PRE-EXISTING count + - Group issues by file and fix type + - Provide code snippets ready to apply (current code → fixed code) + - **DO NOT include "Next Steps" or "Conclusion" sections** - just present the analysis + + **Report Structure:** + ``` + # API Lint Diff Analysis Report + + **Generated:** [date] + **Baseline:** main branch (X issues) + **Current:** [branch name] (Y issues) + **Status:** [status icon and message based on logic below] + + **Status Logic:** + - ✅ PASSED: 0 new issues (fixed issues are OK) + - ⚠️ WARN: 0 new issues but has pre-existing issues + - ❌ FAIL: Has new issues that must be fixed + + ## Executive Summary + - Baseline issues: X + - Current issues: Y + - **FIXED**: F (issues resolved in this branch) + - **NEW**: N (issues introduced in this branch) + - **PRE-EXISTING**: P (issues that still remain) + - False positives (IGNORED): Z + + ## FIXED ISSUES (F issues) + + [List of issues that were fixed in this branch - show the baseline line numbers] + + ## NEW ISSUES (N issues) + + [List of issues introduced in this branch - these MUST be fixed] + + ## PRE-EXISTING ISSUES (P issues) + + [List of issues that existed before and still exist - can be fixed separately] + + --- + + ## DETAILED ANALYSIS FOR ISSUES NEEDING FIXES + + ### Category 1: [Issue Type] (N issues) - [BREAKING/NON-BREAKING] + + #### File: [filename] + + **[Issue #]. Line X - [Field Name]** + ```go + // CURRENT: + [current code] + + // FIX: + [fixed code] + ``` + **Usage Analysis:** + - Found N occurrences in codebase + - [Specific usage example 1]: path/file.go:123 + - [Specific usage example 2]: path/file.go:456 + - Pattern: [always set / sometimes set / checked for zero / etc.] + + **Why:** [Recommendation based on usage analysis with evidence] + **Breaking:** [YES/NO] ([detailed reason with impact]) + + [Repeat for all issues] + + ## Summary of Breaking Changes + [Table of breaking changes if any] + ``` + +## Related Documentation + +- [Kubernetes API Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) +- [kube-api-linter](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/code-generator/cmd/kube-api-linter) +- AGENTS.md in this repository for understanding operator patterns diff --git a/.github/workflows/api-diff-lint.yaml b/.github/workflows/api-diff-lint.yaml new file mode 100644 index 0000000000..94116db48a --- /dev/null +++ b/.github/workflows/api-diff-lint.yaml @@ -0,0 +1,41 @@ +name: api-diff-lint + +on: + pull_request: + +jobs: + lint-api-diff: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Fetch all history for all branches (needed for API diff) + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run API diff linting checks + id: lint-api-diff + continue-on-error: true + run: make lint-api-diff + + - name: Check for override label + if: ${{ steps.lint-api-diff.outcome == 'failure' }} + run: | + if gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name' | grep -q "${OVERRIDE_LABEL}"; then + echo "Found ${OVERRIDE_LABEL} label, overriding failed results." + exit 0 + else + echo "No ${OVERRIDE_LABEL} label found, failing the job." + exit 1 + fi + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + PR: ${{ github.event.pull_request.number }} + OVERRIDE_LABEL: "api-diff-lint-override" + diff --git a/.github/workflows/crd-diff.yaml b/.github/workflows/crd-diff.yaml index be973786ef..7be4d0b1ee 100644 --- a/.github/workflows/crd-diff.yaml +++ b/.github/workflows/crd-diff.yaml @@ -8,13 +8,29 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-go@v6 with: go-version-file: go.mod - - name: Run make verify-crd-compatibility + id: verify-crd-compatibility + continue-on-error: true run: | make verify-crd-compatibility \ CRD_DIFF_ORIGINAL_REF="git://${{ github.event.pull_request.base.sha }}?path=" \ CRD_DIFF_UPDATED_REF="git://${{ github.event.pull_request.head.sha }}?path=" + - name: Check for override label + if: ${{ steps.verify-crd-compatibility.outcome == 'failure' }} + run: | + if gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name' | grep -q "${OVERRIDE_LABEL}"; then + echo "Found ${OVERRIDE_LABEL} label, overriding failed results." + exit 0 + else + echo "No ${OVERRIDE_LABEL} label found, failing the job." + exit 1 + fi + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + PR: ${{ github.event.pull_request.number }} + OVERRIDE_LABEL: "crd-diff-override" \ No newline at end of file diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 6055cd8595..d073771934 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -59,13 +59,13 @@ jobs: make ${{ matrix.test.make-target }} fi - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 if: failure() && matrix.test.use-artifacts == true with: name: ${{ matrix.test.name }}-artifacts path: /tmp/artifacts/ - - uses: codecov/codecov-action@v5.5.1 + - uses: codecov/codecov-action@v5.5.2 if: matrix.test.use-codecov == true with: disable_search: true diff --git a/.github/workflows/files-diff.yaml b/.github/workflows/files-diff.yaml new file mode 100644 index 0000000000..731a1fda3e --- /dev/null +++ b/.github/workflows/files-diff.yaml @@ -0,0 +1,49 @@ +name: file-diff + +on: + pull_request: + +permissions: + pull-requests: write + +jobs: + check-networkpolicy-changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dorny/paths-filter@v3 + id: filter + with: + list-files: shell + filters: | + networkpolicy: + - 'helm/olmv1/templates/networkpolicy/**' + + - name: Comment on PR if NetworkPolicy files changed + if: steps.filter.outputs.networkpolicy == 'true' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: networkpolicy-changes + message: | + ## ⚠️ NetworkPolicy Changes Detected + + This PR modifies NetworkPolicy files which affect cluster security. + + **Changed files:** + ``` + ${{ steps.filter.outputs.networkpolicy_files }} + ``` + + **Please ensure:** + - These changes are intentional and reviewed carefully + - The OPA policies in `hack/conftest/policy/` are updated accordingly + - The changes have been validated with `make lint-helm` + + NetworkPolicy changes require careful review as they affect cluster security. + + - name: Fail if NetworkPolicy files changed + if: steps.filter.outputs.networkpolicy == 'true' + run: | + echo "::error::NetworkPolicy files have been modified. See PR comment for details." + exit 1 diff --git a/.github/workflows/go-verdiff.yaml b/.github/workflows/go-verdiff.yaml index f8fb06b6c3..62f89bf9f3 100644 --- a/.github/workflows/go-verdiff.yaml +++ b/.github/workflows/go-verdiff.yaml @@ -11,12 +11,24 @@ jobs: with: fetch-depth: 0 - name: Check golang version + id: check-version + continue-on-error: true run: | - export LABELS="$(gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name')" hack/tools/check-go-version.sh -b "${{ github.event.pull_request.base.sha }}" shell: bash + - name: Check for override label + if: ${{ steps.check-version.outcome == 'failure' }} + run: | + if gh api repos/$OWNER/$REPO/pulls/$PR --jq '.labels.[].name' | grep -q "${OVERRIDE_LABEL}"; then + echo "Found ${OVERRIDE_LABEL} label, overriding failed results." + exit 0 + else + echo "No ${OVERRIDE_LABEL} label found, failing the job." + exit 1 + fi env: GH_TOKEN: ${{ github.token }} OWNER: ${{ github.repository_owner }} REPO: ${{ github.event.repository.name }} PR: ${{ github.event.pull_request.number }} + OVERRIDE_LABEL: "go-verdiff-override" \ No newline at end of file diff --git a/.github/workflows/test-regression.yaml b/.github/workflows/test-regression.yaml index 8e0dd93e98..8b95aed964 100644 --- a/.github/workflows/test-regression.yaml +++ b/.github/workflows/test-regression.yaml @@ -23,7 +23,7 @@ jobs: run: | make test-regression - - uses: codecov/codecov-action@v5.5.1 + - uses: codecov/codecov-action@v5.5.2 with: disable_search: true files: coverage/regression.out diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index 390faf2771..48e736e027 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -23,7 +23,7 @@ jobs: run: | make test-unit - - uses: codecov/codecov-action@v5.5.1 + - uses: codecov/codecov-action@v5.5.2 with: disable_search: true files: coverage/unit.out diff --git a/.goreleaser.yml b/.goreleaser.yml index 7200142142..e2807ca41f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,4 @@ +version: 2 before: hooks: - go mod tidy @@ -116,7 +117,7 @@ docker_manifests: checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: use: github-native disable: '{{ ne .Env.ENABLE_RELEASE_PIPELINE "true" }}' diff --git a/.tilt-support b/.tilt-support index 9cb01b1526..dcd827d04c 100644 --- a/.tilt-support +++ b/.tilt-support @@ -14,7 +14,7 @@ def deploy_cert_manager_if_needed(): docker_build( ref='helper', context='.', - build_args={'GO_VERSION': '1.24'}, + build_args={'GO_VERSION': '1.25'}, dockerfile_contents=''' ARG GO_VERSION FROM golang:${GO_VERSION} diff --git a/AGENTS.md b/AGENTS.md index e1a7626f6d..07dd2e6be8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -289,7 +289,6 @@ Two manifest variants exist: - `/go.mod` - Dependencies (use `make tidy`, avoid Go version bumps without discussion) - `/PROJECT` - Kubebuilder project config - `/OWNERS` & `/OWNERS_ALIASES` - Maintainer lists -- `/CODEOWNERS` - Code ownership - `/mkdocs.yml` - Documentation site config - `/.bingo/*.mod` - Tool dependencies (managed by bingo) - `/.golangci.yaml` - Linter configuration diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 1578d1c0f0..0000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @operator-framework/operator-controller-maintainers diff --git a/Makefile b/Makefile index c7d9ea5cfe..41397e9cc4 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,9 @@ ifeq ($(origin KIND_CLUSTER_NAME), undefined) KIND_CLUSTER_NAME := operator-controller endif +ifeq ($(origin KIND_CONFIG), undefined) +KIND_CONFIG := ./kind-config/kind-config.yaml +endif ifneq (, $(shell command -v docker 2>/dev/null)) CONTAINER_RUNTIME := docker @@ -118,9 +121,25 @@ 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 +.PHONY: lint-helm +lint-helm: $(HELM) $(CONFTEST) #HELP Run helm linter helm lint helm/olmv1 helm lint helm/prometheus + (set -euo pipefail; helm template olmv1 helm/olmv1; helm template prometheus helm/prometheus) | $(CONFTEST) test --policy hack/conftest/policy/ --combine -n main -n prometheus - + +.PHONY: lint-deployed-resources +lint-deployed-resources: $(KUBE_SCORE) #EXHELP Lint deployed resources. + (for ns in $$(printf "olmv1-system\n%s\n" "$(CATD_NAMESPACE)" | uniq); do \ + for resource in $$(kubectl api-resources --verbs=list --namespaced -o name); do \ + kubectl get $$resource -n $$ns -o yaml ; \ + echo "---" ; \ + done \ + done) | $(KUBE_SCORE) score - \ + `# TODO: currently these checks are failing, decide if resources should be fixed for them to pass (https://github.com/operator-framework/operator-controller/issues/2398)` \ + --ignore-test container-resources \ + --ignore-test container-image-pull-policy \ + --ignore-test container-ephemeral-storage-request-and-limit \ + --ignore-test container-security-context-user-group-id .PHONY: custom-linter-build custom-linter-build: #EXHELP Build custom linter @@ -130,6 +149,10 @@ custom-linter-build: #EXHELP Build custom linter lint-custom: custom-linter-build #EXHELP Call custom linter for the project go vet -tags=$(GO_BUILD_TAGS) -vettool=./bin/custom-linter ./... +.PHONY: lint-api-diff +lint-api-diff: $(GOLANGCI_LINT) #HELP Validate API changes using kube-api-linter with diff-aware analysis + hack/api-lint-diff/run.sh + .PHONY: k8s-pin k8s-pin: #EXHELP Pin k8s staging modules based on k8s.io/kubernetes version (in go.mod or from K8S_IO_K8S_VERSION env var) and run go mod tidy. K8S_IO_K8S_VERSION='$(K8S_IO_K8S_VERSION)' go run hack/tools/k8smaintainer/main.go @@ -161,9 +184,10 @@ $(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) +$(MANIFESTS): $(HELM) $(CONFTEST) @mkdir -p $(MANIFEST_HOME) $(HELM) template olmv1 helm/olmv1 $(addprefix --values ,$($@)) $(addprefix --set ,$(HELM_SETTINGS)) > $@ + $(CONFTEST) test --policy hack/conftest/policy/ -n main --combine $@ # Generate manifests stored in source-control .PHONY: manifests @@ -178,7 +202,7 @@ generate: $(CONTROLLER_GEN) #EXHELP Generate code containing DeepCopy, DeepCopyI $(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: verify -verify: k8s-pin kind-verify-versions fmt generate manifests update-tls-profiles crd-ref-docs verify-bingo #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 update-registryv1-bundle-schema verify-bingo #HELP Verify all generated code is up-to-date. Runs k8s-pin instead of just tidy. git diff --exit-code .PHONY: verify-bingo @@ -198,6 +222,10 @@ fmt: $(YAMLFMT) #EXHELP Formats code update-tls-profiles: $(GOJQ) #EXHELP Update TLS profiles from the Mozilla wiki env JQ=$(GOJQ) hack/tools/update-tls-profiles.sh +.PHONY: update-registryv1-bundle-schema +update-registryv1-bundle-schema: #EXHELP Update registry+v1 bundle configuration JSON schema + hack/tools/update-registryv1-bundle-schema.sh + .PHONY: verify-crd-compatibility CRD_DIFF_ORIGINAL_REF := git://main?path= CRD_DIFF_UPDATED_REF := file:// @@ -215,7 +243,7 @@ test: manifests generate fmt lint test-unit test-e2e test-regression #HELP Run a .PHONY: e2e e2e: #EXHELP Run the e2e tests. - go test -count=1 -v ./test/e2e/... + go test -count=1 -v ./test/e2e/features_test.go E2E_REGISTRY_NAME := docker-registry E2E_REGISTRY_NAMESPACE := operator-controller-e2e @@ -263,7 +291,7 @@ 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/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) + $(KIND) load docker-image $(E2E_REGISTRY_IMAGE) --name $(KIND_CLUSTER_NAME) ./testdata/build-test-registry.sh $(E2E_REGISTRY_NAMESPACE) $(E2E_REGISTRY_NAME) $(E2E_REGISTRY_IMAGE) # When running the e2e suite, you can set the ARTIFACT_PATH variable to the absolute path @@ -282,6 +310,7 @@ test-e2e: run-internal image-registry prometheus e2e e2e-coverage kind-clean #HE .PHONY: test-experimental-e2e test-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) test-experimental-e2e: KIND_CLUSTER_NAME := operator-controller-e2e +test-experimental-e2e: KIND_CONFIG := ./kind-config/kind-config-2node.yaml test-experimental-e2e: GO_BUILD_EXTRA_FLAGS := -cover test-experimental-e2e: COVERAGE_NAME := experimental-e2e test-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) @@ -382,8 +411,8 @@ stop-profiling: build-test-profiler #EXHELP Stop profiling and generate analysis .PHONY: kind-load kind-load: $(KIND) #EXHELP Loads the currently constructed images into the KIND cluster. - $(CONTAINER_RUNTIME) save $(OPCON_IMG) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) - $(CONTAINER_RUNTIME) save $(CATD_IMG) | $(KIND) load image-archive /dev/stdin --name $(KIND_CLUSTER_NAME) + $(KIND) load docker-image $(OPCON_IMG) --name $(KIND_CLUSTER_NAME) + $(KIND) load docker-image $(CATD_IMG) --name $(KIND_CLUSTER_NAME) .PHONY: kind-deploy kind-deploy: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) @@ -408,8 +437,9 @@ kind-deploy-experimental: manifests .PHONY: 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) --config ./kind-config.yaml + $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config $(KIND_CONFIG) $(KIND) export kubeconfig --name $(KIND_CLUSTER_NAME) + kubectl wait --for=condition=Ready nodes --all --timeout=2m .PHONY: kind-clean kind-clean: $(KIND) #EXHELP Delete the kind cluster. @@ -469,7 +499,7 @@ go-build-linux: export GOARCH=amd64 go-build-linux: $(BINARIES) .PHONY: run-internal -run-internal: docker-build kind-cluster kind-load kind-deploy wait +run-internal: docker-build kind-cluster kind-load kind-deploy lint-deployed-resources wait .PHONY: run run: SOURCE_MANIFEST := $(STANDARD_MANIFEST) diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES index 3d13816894..c2c73d7e8f 100644 --- a/OWNERS_ALIASES +++ b/OWNERS_ALIASES @@ -1,5 +1,6 @@ aliases: olmv1-approvers: + - camilamacedo86 - grokspawn - joelanford - kevinrizza @@ -27,14 +28,10 @@ aliases: api-approvers: - grokspawn - - thetechnick catalogd-approvers: - grokspawn - operator-controller-approvers: - - thetechnick - cmd-approvers: - grokspawn @@ -50,4 +47,3 @@ aliases: docs-draft-approvers: - camilamacedo86 - grokspawn - - thetechnick diff --git a/api/v1/clustercatalog_types.go b/api/v1/clustercatalog_types.go index c18fa3c7e6..7afe631db4 100644 --- a/api/v1/clustercatalog_types.go +++ b/api/v1/clustercatalog_types.go @@ -51,7 +51,7 @@ const ( //+kubebuilder:printcolumn:name="Serving",type=string,JSONPath=`.status.conditions[?(@.type=="Serving")].status` //+kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp` -// ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. +// ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. // For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs type ClusterCatalog struct { metav1.TypeMeta `json:",inline"` @@ -60,16 +60,14 @@ type ClusterCatalog struct { // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata metav1.ObjectMeta `json:"metadata"` - // 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. - // +kubebuilder:validation:Required + // spec is a required field that defines the desired state of the ClusterCatalog. + // The controller ensures that the catalog is unpacked and served over the catalog content HTTP server. + // +required Spec ClusterCatalogSpec `json:"spec"` - // 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 + // status contains the following information about the state of the ClusterCatalog: + // - Whether the catalog contents are being served via the catalog content HTTP server + // - Whether the ClusterCatalog is progressing to a new state // - A reference to the source from which the catalog contents were retrieved // +optional Status ClusterCatalogStatus `json:"status,omitempty"` @@ -87,21 +85,18 @@ type ClusterCatalogList struct { // items is a list of ClusterCatalogs. // items is required. - // +kubebuilder:validation:Required + // +required Items []ClusterCatalog `json:"items"` } // ClusterCatalogSpec defines the desired state of ClusterCatalog type ClusterCatalogSpec struct { - // 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. + // source is a required field that defines the source of a catalog. + // A catalog contains information on content that can be installed on a cluster. + // The catalog source makes catalog contents discoverable and usable by other on-cluster components. + // These components can present the content in a GUI dashboard or install 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: // @@ -110,47 +105,43 @@ type ClusterCatalogSpec struct { // image: // ref: quay.io/operatorhubio/catalog:latest // - // +kubebuilder:validation:Required + // +required Source CatalogSource `json:"source"` - // priority allows the user to define a priority for a ClusterCatalog. - // priority is optional. + // priority is an optional field that defines a priority for this ClusterCatalog. // - // 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. + // Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + // Higher numbers mean 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. + // Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + // Clients should prompt users for additional input to break the tie. // - // When omitted, the default priority is 0 because that is the zero value of integers. + // When omitted, the default priority is 0. // - // 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. + // Use negative numbers to specify a priority lower than the default. + // Use positive numbers to specify a priority higher than the default. // // The lowest possible value is -2147483648. // The highest possible value is 2147483647. // // +kubebuilder:default:=0 - // +kubebuilder:validation:minimum:=-2147483648 - // +kubebuilder:validation:maximum:=2147483647 + // +kubebuilder:validation:Minimum:=-2147483648 + // +kubebuilder:validation:Maximum:=2147483647 // +optional Priority int32 `json:"priority"` - // availabilityMode allows users to define how the ClusterCatalog is made available to clients on the cluster. - // availabilityMode is optional. + // availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. // - // Allowed values are "Available" and "Unavailable" and omitted. + // Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + // Clients 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. + // When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + // Treat this the same as if the ClusterCatalog does not exist. + // Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. // // +kubebuilder:validation:Enum:="Unavailable";"Available" // +kubebuilder:default:="Available" @@ -160,24 +151,23 @@ type ClusterCatalogSpec struct { // ClusterCatalogStatus defines the observed state of ClusterCatalog type ClusterCatalogStatus struct { - // conditions is a representation of the current state for this ClusterCatalog. + // conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + // - When status is True and reason is Available, the catalog contents are being served. + // - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + // - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + // The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + // - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + // - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + // - When status is False and reason is Blocked, an error occurred 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. + // If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + // - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + // - The Progressing condition is True with reason Retrying because the system is working to serve the new version. // // +listType=map // +listMapKey=type @@ -189,32 +179,27 @@ type ClusterCatalogStatus struct { // urls contains the URLs that can be used to access the catalog. // +optional URLs *ClusterCatalogURLs `json:"urls,omitempty"` - // 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". + // lastUnpacked represents the last time the catalog contents were extracted from their source format. + // For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + // This extraction from the source format is called "unpacking". // +optional LastUnpacked *metav1.Time `json:"lastUnpacked,omitempty"` } // ClusterCatalogURLs contains the URLs that can be used to access the catalog. type ClusterCatalogURLs struct { - // base is a cluster-internal URL that provides endpoints for - // accessing the content of the catalog. + // base is a cluster-internal URL that provides endpoints for accessing the catalog content. // - // It is expected that clients append the path for the endpoint they wish - // to access. + // Clients should append the path for the endpoint they want to access. // - // Currently, only a single endpoint is served and is accessible at the path - // /api/v1. + // 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 + // - /all - this endpoint returns the entire catalog contents in the FBC format // - // As the needs of users and clients of the evolve, new endpoints may be added. + // New endpoints may be added as needs evolve. // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:MaxLength:=525 // +kubebuilder:validation:XValidation:rule="isURL(self)",message="must be a valid URL" // +kubebuilder:validation:XValidation:rule="isURL(self) ? (url(self).getScheme() == \"http\" || url(self).getScheme() == \"https\") : true",message="scheme must be either http or https" @@ -226,20 +211,19 @@ type ClusterCatalogURLs struct { // +union // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Image' ? has(self.image) : !has(self.image)",message="image is required when source type is Image, and forbidden otherwise" type CatalogSource struct { - // type is a reference to the type of source the catalog is sourced from. - // type is required. + // type is a required field that specifies the type of source for the catalog. // // The only allowed value is "Image". // - // When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + // When set to "Image", the ClusterCatalog content is 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. // // +unionDiscriminator // +kubebuilder:validation:Enum:="Image" - // +kubebuilder:validation:Required + // +required Type SourceType `json:"type"` - // 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. + // image configures how catalog contents are sourced from an OCI image. + // It is required when type is Image, and forbidden otherwise. // +optional Image *ImageSource `json:"image,omitempty"` } @@ -249,28 +233,27 @@ type CatalogSource struct { // +union // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Image' ? has(self.image) : !has(self.image)",message="image is required when source type is Image, and forbidden otherwise" type ResolvedCatalogSource struct { - // type is a reference to the type of source the catalog is sourced from. - // type is required. + // type is a required field that specifies the type of source for the catalog. // // The only allowed value is "Image". // - // When set to "Image", information about the resolved image source will be set in the 'image' field. + // When set to "Image", information about the resolved image source is set in the image field. // // +unionDiscriminator // +kubebuilder:validation:Enum:="Image" - // +kubebuilder:validation:Required + // +required Type SourceType `json:"type"` - // 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. + // image contains resolution information for a catalog sourced from an image. + // It must be set when type is Image, and forbidden otherwise. Image *ResolvedImageSource `json:"image"` } // ResolvedImageSource provides information about the resolved source of a Catalog sourced from an image. type ResolvedImageSource struct { // 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. - // +kubebuilder:validation:Required + // The digest format allows you to use other tooling to fetch the exact OCI manifests + // that were used to extract the catalog contents. + // +required // +kubebuilder:validation:MaxLength:=1000 // +kubebuilder:validation:XValidation: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="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." @@ -287,11 +270,10 @@ type ResolvedImageSource struct { // reject the resource since there is no use in polling a digest-based image reference. // +kubebuilder:validation:XValidation:rule="self.ref.find('(@.*:)') != \"\" ? !has(self.pollIntervalMinutes) : true",message="cannot specify pollIntervalMinutes while using digest-based image" type ImageSource struct { - // 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. + // ref is a required field that defines the reference to a container image containing catalog contents. + // It cannot be more than 1000 characters. // - // A reference can be broken down into 3 parts - the domain, name, and identifier. + // A reference has 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. @@ -325,7 +307,7 @@ type ImageSource struct { // 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" // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:MaxLength:=1000 // +kubebuilder:validation:XValidation: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="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." @@ -337,11 +319,10 @@ type ImageSource struct { // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find(':.*$').matches(':[0-9A-Fa-f]*$') : true",message="digest is not valid. the encoded string must only contain hex characters (A-F, a-f, 0-9)" Ref string `json:"ref"` - // 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. + // pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + // You cannot specify pollIntervalMinutes when ref is a digest-based reference. // - // When omitted, the image will not be polled for new content. + // When omitted, the image is not polled for new content. // +kubebuilder:validation:Minimum:=1 // +optional PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"` diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 8c99cf67b1..7b0a39ef12 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -48,39 +48,38 @@ const ( // 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. + // namespace specifies a Kubernetes namespace. + // This is the namespace where 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 + // The namespace field 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 == oldSelf",message="namespace is immutable" // +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 + // +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. + // serviceAccount specifies 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. + // The serviceAccount field is required. // - // +kubebuilder:validation:Required + // +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. + // source is required and selects the installation source of content for this ClusterExtension. + // Set the sourceType field to perform the selection. // - // Catalog is currently the only implemented sourceType, and setting the - // sourcetype to "Catalog" requires the catalog field to also be defined. + // Catalog is currently the only implemented sourceType. + // Setting sourceType to "Catalog" requires the catalog field to also be defined. // // Below is a minimal example of a source definition (in yaml): // @@ -89,26 +88,36 @@ type ClusterExtensionSpec struct { // catalog: // packageName: example-package // - // +kubebuilder:validation:Required + // +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. + // install is optional and configures installation options for the ClusterExtension, + // such as the pre-flight check configuration. // // +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 optional and specifies bundle-specific configuration. + // Configuration is bundle-specific and a bundle may provide a configuration schema. + // When not specified, the default configuration of the resolved bundle is 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. + // a configuration schema the bundle is deemed to not be configurable. More information on how + // to configure bundles can be found in the OLM documentation associated with your current OLM version. // - // // +optional Config *ClusterExtensionConfig `json:"config,omitempty"` + + // progressDeadlineMinutes is an optional field that defines the maximum period + // of time in minutes after which an installation should be considered failed and + // require manual intervention. This functionality is disabled when no value + // is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + // + // +kubebuilder:validation:Minimum:=10 + // +kubebuilder:validation:Maximum:=720 + // +optional + // + ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` } const SourceTypeCatalog = "Catalog" @@ -118,22 +127,21 @@ const SourceTypeCatalog = "Catalog" // +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. + // sourceType is required and specifies the type of install source. // - // Allowed values are "Catalog" + // The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + // is fetched from ClusterCatalog resources on the cluster. // When using the Catalog sourceType, the catalog field must also be set. // // +unionDiscriminator // +kubebuilder:validation:Enum:="Catalog" - // +kubebuilder:validation:Required + // +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. + // catalog configures how information is sourced from a catalog. + // It is required when sourceType is "Catalog", and forbidden otherwise. // // +optional Catalog *CatalogFilter `json:"catalog,omitempty"` @@ -145,11 +153,11 @@ type SourceConfig struct { // +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. + // preflight is optional and configures the checks that 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. + // When not specified, the default configuration is used. // // +optional Preflight *PreflightConfig `json:"preflight,omitempty"` @@ -161,22 +169,20 @@ type ClusterExtensionInstallConfig struct { // +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. + // configType is required and specifies the type of configuration source. // - // Allowed values are "Inline" + // The only allowed value is "Inline". // - // When this field is set to "Inline", the cluster extension configuration is defined inline within the - // ClusterExtension resource. + // When set to "Inline", the cluster extension configuration is defined inline within the ClusterExtension resource. // // +unionDiscriminator // +kubebuilder:validation:Enum:="Inline" - // +kubebuilder:validation:Required + // +required ConfigType ClusterExtensionConfigType `json:"configType"` - // inline contains JSON or YAML values specified directly in the - // ClusterExtension. + // inline contains JSON or YAML values specified directly in the ClusterExtension. // - // inline is used to specify arbitrary configuration values for the ClusterExtension. + // It is used to specify arbitrary configuration values for the ClusterExtension. // It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. // The configuration values are validated at runtime against a JSON schema provided by the bundle. // @@ -189,13 +195,12 @@ type ClusterExtensionConfig struct { // 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 specifies 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. + // It 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 @@ -215,15 +220,16 @@ type CatalogFilter struct { // +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 + // +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. + // version is an optional semver constraint (a specific version or range of versions). + // When unspecified, the latest version available is 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 (||). + // Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + // known as comparison strings. + // You can add additional comparison strings using the OR operator (||). // // # Range Comparisons // @@ -297,25 +303,24 @@ type CatalogFilter struct { // +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. + // channels is optional and specifies 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. + // 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. + // 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. + // You can specify no more than 256 channels. // - // 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: + // When specified, it constrains 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 + // - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + // - Automatic upgrades are 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. + // When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. // // Some examples of valid values are: // - 1.1.x @@ -342,33 +347,28 @@ type CatalogFilter struct { // +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. + // selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. // - // When unspecified, all ClusterCatalogs will be used in - // the bundle selection process. + // When unspecified, all ClusterCatalogs are 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. + // upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + // are enforced for the package referenced in the packageName field. // - // Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + // Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + // This allows 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. + // Use this option only if you have independently verified the changes. // - // When this field is omitted, the default value is "CatalogProvided". + // When omitted, the default value is "CatalogProvided". // // +kubebuilder:validation:Enum:=CatalogProvided;SelfCertified // +kubebuilder:default:=CatalogProvided @@ -378,16 +378,14 @@ type CatalogFilter struct { // 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. + // name is a required, immutable reference to the name of the ServiceAccount 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. + // The name field 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 @@ -405,7 +403,7 @@ type ServiceAccountReference struct { // +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 + // +required Name string `json:"name"` } @@ -413,29 +411,27 @@ type ServiceAccountReference struct { // // +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. + // crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + // before upgrades of installed content. // - // The CRD Upgrade Safety pre-flight check safeguards from unintended - // consequences of upgrading a CRD, such as data loss. + // 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. + // enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + // Use this option 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. + // When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. // // +kubebuilder:validation:Enum:="None";"Strict" - // +kubebuilder:validation:Required + // +required Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"` } @@ -455,19 +451,18 @@ const ( // 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. + // 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 + // +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/. + // version is required and references the version that this bundle represents. + // It follows the semantic versioning standard as defined in https://semver.org/. // - // +kubebuilder:validation:Required + // +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"` } @@ -489,11 +484,13 @@ type RevisionStatus struct { // ClusterExtensionStatus defines the observed state of a ClusterExtension. type ClusterExtensionStatus struct { + // conditions represents the current state of the ClusterExtension. + // // 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 Installed condition represents whether 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. @@ -503,12 +500,12 @@ type ClusterExtensionStatus struct { // When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out. // // - // 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. + // When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + // These are indications from a package owner to guide users away from a particular package, channel, or bundle: + // - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + // - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + // - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + // - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. // // +listType=map // +listMapKey=type @@ -531,12 +528,12 @@ type ClusterExtensionStatus struct { // 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. + // bundle is required and 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. + // 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 + // +required Bundle BundleMetadata `json:"bundle"` } @@ -551,7 +548,11 @@ type ClusterExtensionInstallStatus struct { // ClusterExtension is the Schema for the clusterextensions API type ClusterExtension struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` + + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec is an optional field that defines the desired state of the ClusterExtension. @@ -574,7 +575,7 @@ type ClusterExtensionList struct { // items is a required list of ClusterExtension objects. // - // +kubebuilder:validation:Required + // +required Items []ClusterExtension `json:"items"` } diff --git a/api/v1/clusterextensionrevision_types.go b/api/v1/clusterextensionrevision_types.go index 368a8fca88..0f41ab3195 100644 --- a/api/v1/clusterextensionrevision_types.go +++ b/api/v1/clusterextensionrevision_types.go @@ -61,7 +61,7 @@ type ClusterExtensionRevisionSpec struct { // Each ClusterExtensionRevision belonging to the same parent ClusterExtension must have a unique revision number. // The revision number must always be the previous revision number plus one, or 1 for the first revision. // - // +kubebuilder:validation:Required + // +required // +kubebuilder:validation:Minimum:=1 // +kubebuilder:validation:XValidation:rule="self == oldSelf", message="revision is immutable" Revision int64 `json:"revision"` @@ -87,6 +87,17 @@ type ClusterExtensionRevisionSpec struct { // +listMapKey=name // +optional Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"` + + // progressDeadlineMinutes is an optional field that defines the maximum period + // of time in minutes after which an installation should be considered failed and + // require manual intervention. This functionality is disabled when no value + // is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + // + // +kubebuilder:validation:Minimum:=10 + // +kubebuilder:validation:Maximum:=720 + // +optional + // + ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` } // ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision. @@ -215,7 +226,11 @@ type ClusterExtensionRevisionStatus struct { // or reconfigured. Once the latest revision has rolled out successfully, previous active revisions are archived for // posterity. type ClusterExtensionRevision struct { - metav1.TypeMeta `json:",inline"` + metav1.TypeMeta `json:",inline"` + + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec defines the desired state of the ClusterExtensionRevision. @@ -238,7 +253,7 @@ type ClusterExtensionRevisionList struct { // items is a required list of ClusterExtensionRevision objects. // - // +kubebuilder:validation:Required + // +required Items []ClusterExtensionRevision `json:"items"` } diff --git a/api/v1/common_types.go b/api/v1/common_types.go index 115836b10c..53215dbdee 100644 --- a/api/v1/common_types.go +++ b/api/v1/common_types.go @@ -24,14 +24,18 @@ const ( ReasonAbsent = "Absent" // Progressing reasons - ReasonRollingOut = "RollingOut" - ReasonRetrying = "Retrying" - ReasonBlocked = "Blocked" + ReasonRollingOut = "RollingOut" + ReasonRetrying = "Retrying" + ReasonBlocked = "Blocked" + ReasonInvalidConfiguration = "InvalidConfiguration" // Deprecation reasons - ReasonDeprecated = "Deprecated" + ReasonDeprecated = "Deprecated" + ReasonNotDeprecated = "NotDeprecated" + ReasonDeprecationStatusUnknown = "DeprecationStatusUnknown" // Common reasons - ReasonSucceeded = "Succeeded" - ReasonFailed = "Failed" + ReasonSucceeded = "Succeeded" + ReasonFailed = "Failed" + ReasonProgressDeadlineExceeded = "ProgressDeadlineExceeded" ) diff --git a/api/v1/validation_test.go b/api/v1/validation_test.go new file mode 100644 index 0000000000..bbd755c3c0 --- /dev/null +++ b/api/v1/validation_test.go @@ -0,0 +1,176 @@ +package v1 + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestValidate(t *testing.T) { + type args struct { + object any + skipDefaulting bool + } + type want struct { + valid bool + } + type testCase struct { + args args + want want + } + defaultExtensionSpec := func(s *ClusterExtensionSpec) *ClusterExtensionSpec { + s.Namespace = "ns" + s.ServiceAccount = ServiceAccountReference{ + Name: "sa", + } + s.Source = SourceConfig{ + SourceType: SourceTypeCatalog, + Catalog: &CatalogFilter{ + PackageName: "test", + }, + } + return s + } + defaultRevisionSpec := func(s *ClusterExtensionRevisionSpec) *ClusterExtensionRevisionSpec { + s.Revision = 1 + return s + } + c := newClient(t) + i := 0 + + for name, tc := range map[string]testCase{ + "ClusterExtension: invalid progress deadline < 10": { + args: args{ + object: ClusterExtensionSpec{ + ProgressDeadlineMinutes: 9, + }, + }, + want: want{valid: false}, + }, + "ClusterExtension: valid progress deadline = 10": { + args: args{ + object: ClusterExtensionSpec{ + ProgressDeadlineMinutes: 10, + }, + }, + want: want{valid: true}, + }, + "ClusterExtension: valid progress deadline = 360": { + args: args{ + object: ClusterExtensionSpec{ + ProgressDeadlineMinutes: 360, + }, + }, + want: want{valid: true}, + }, + "ClusterExtension: valid progress deadline = 720": { + args: args{ + object: ClusterExtensionSpec{ + ProgressDeadlineMinutes: 720, + }, + }, + want: want{valid: true}, + }, + "ClusterExtension: invalid progress deadline > 720": { + args: args{ + object: ClusterExtensionSpec{ + ProgressDeadlineMinutes: 721, + }, + }, + want: want{valid: false}, + }, + "ClusterExtension: no progress deadline set": { + args: args{ + object: ClusterExtensionSpec{}, + }, + want: want{valid: true}, + }, + "ClusterExtensionRevision: invalid progress deadline < 10": { + args: args{ + object: ClusterExtensionRevisionSpec{ + ProgressDeadlineMinutes: 9, + }, + }, + want: want{valid: false}, + }, + "ClusterExtensionRevision: valid progress deadline = 10": { + args: args{ + object: ClusterExtensionRevisionSpec{ + ProgressDeadlineMinutes: 10, + }, + }, + want: want{valid: true}, + }, + "ClusterExtensionRevision: valid progress deadline = 360": { + args: args{ + object: ClusterExtensionRevisionSpec{ + ProgressDeadlineMinutes: 360, + }, + }, + want: want{valid: true}, + }, + "ClusterExtensionRevision: valid progress deadline = 720": { + args: args{ + object: ClusterExtensionRevisionSpec{ + ProgressDeadlineMinutes: 720, + }, + }, + want: want{valid: true}, + }, + "ClusterExtensionRevision: invalid progress deadline > 720": { + args: args{ + object: ClusterExtensionRevisionSpec{ + ProgressDeadlineMinutes: 721, + }, + }, + want: want{valid: false}, + }, + "ClusterExtensionRevision: no progress deadline set": { + args: args{ + object: ClusterExtensionRevisionSpec{}, + }, + want: want{valid: true}, + }, + } { + t.Run(name, func(t *testing.T) { + var obj client.Object + switch s := tc.args.object.(type) { + case ClusterExtensionSpec: + ce := &ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("ce-%d", i), + }, + Spec: s, + } + if !tc.args.skipDefaulting { + defaultExtensionSpec(&ce.Spec) + } + obj = ce + case ClusterExtensionRevisionSpec: + cer := &ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("cer-%d", i), + }, + Spec: s, + } + if !tc.args.skipDefaulting { + defaultRevisionSpec(&cer.Spec) + } + obj = cer + default: + t.Fatalf("unknown type %T", s) + } + i++ + err := c.Create(t.Context(), obj) + if tc.want.valid && err != nil { + t.Fatal("expected create to succeed, but got:", err) + } + if !tc.want.valid && !errors.IsInvalid(err) { + t.Fatal("expected create to fail due to invalid payload, but got:", err) + } + }) + } +} diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index a1d03feee4..dd0bc98f6d 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -42,10 +42,7 @@ import ( _ "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" @@ -601,7 +598,12 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl return err } - // TODO: add support for preflight checks + // determine if PreAuthorizer should be enabled based on feature gate + var preAuth authorization.PreAuthorizer + if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) { + preAuth = authorization.NewRBACPreAuthorizer(c.mgr.GetClient()) + } + // TODO: better scheme handling - which types do we want to support? _ = apiextensionsv1.AddToScheme(c.mgr.GetScheme()) rg := &applier.SimpleRevisionGenerator{ @@ -613,6 +615,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl Scheme: c.mgr.GetScheme(), RevisionGenerator: rg, Preflights: c.preflights, + PreAuthorizer: preAuth, FieldOwner: fmt.Sprintf("%s/clusterextension-controller", fieldOwnerPrefix), } revisionStatesGetter := &controllers.BoxcutterRevisionStatesGetter{Reader: c.mgr.GetClient()} @@ -626,9 +629,9 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl controllers.HandleFinalizers(c.finalizers), controllers.MigrateStorage(storageMigrator), controllers.RetrieveRevisionStates(revisionStatesGetter), - controllers.ResolveBundle(c.resolver), + controllers.ResolveBundle(c.resolver, c.mgr.GetClient()), controllers.UnpackBundle(c.imagePuller, c.imageCache), - controllers.ApplyBundleWithBoxcutter(appl), + controllers.ApplyBundleWithBoxcutter(appl.Apply), } baseDiscoveryClient, err := discovery.NewDiscoveryClientForConfig(c.mgr.GetConfig()) @@ -653,21 +656,29 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl return fmt.Errorf("unable to add tracking cache to manager: %v", err) } + cerCoreClient, err := corev1client.NewForConfig(c.mgr.GetConfig()) + if err != nil { + return fmt.Errorf("unable to create client for ClusterExtensionRevision controller: %w", err) + } + cerTokenGetter := authentication.NewTokenGetter(cerCoreClient, authentication.WithExpirationDuration(1*time.Hour)) + + revisionEngineFactory, err := controllers.NewDefaultRevisionEngineFactory( + c.mgr.GetScheme(), + trackingCache, + discoveryClient, + c.mgr.GetRESTMapper(), + fieldOwnerPrefix, + c.mgr.GetConfig(), + cerTokenGetter, + ) + if err != nil { + return fmt.Errorf("unable to create revision engine factory: %w", err) + } + if err = (&controllers.ClusterExtensionRevisionReconciler{ - Client: c.mgr.GetClient(), - RevisionEngine: machinery.NewRevisionEngine( - machinery.NewPhaseEngine( - machinery.NewObjectEngine( - c.mgr.GetScheme(), trackingCache, c.mgr.GetClient(), - ownerhandling.NewNative(c.mgr.GetScheme()), - machinery.NewComparator(ownerhandling.NewNative(c.mgr.GetScheme()), discoveryClient, c.mgr.GetScheme(), fieldOwnerPrefix), - fieldOwnerPrefix, fieldOwnerPrefix, - ), - validation.NewClusterPhaseValidator(c.mgr.GetRESTMapper(), c.mgr.GetClient()), - ), - validation.NewRevisionValidator(), c.mgr.GetClient(), - ), - TrackingCache: trackingCache, + Client: c.mgr.GetClient(), + RevisionEngineFactory: revisionEngineFactory, + TrackingCache: trackingCache, }).SetupWithManager(c.mgr); err != nil { return fmt.Errorf("unable to setup ClusterExtensionRevision controller: %w", err) } @@ -737,7 +748,7 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{ controllers.HandleFinalizers(c.finalizers), controllers.RetrieveRevisionStates(revisionStatesGetter), - controllers.ResolveBundle(c.resolver), + controllers.ResolveBundle(c.resolver, c.mgr.GetClient()), controllers.UnpackBundle(c.imagePuller, c.imageCache), controllers.ApplyBundle(appl), } diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 866de53279..3ee7f19386 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -46,8 +46,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | 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. | | Required: \{\}
| -| `version` _string_ | 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/. | | Required: \{\}
| +| `name` _string_ | 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. | | Required: \{\}
| +| `version` _string_ | version is required and references the version that this bundle represents.
It follows the semantic versioning standard as defined in https://semver.org/. | | Required: \{\}
| #### CRDUpgradeSafetyEnforcement @@ -80,7 +80,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `enforcement` _[CRDUpgradeSafetyEnforcement](#crdupgradesafetyenforcement)_ | 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]
Required: \{\}
| +| `enforcement` _[CRDUpgradeSafetyEnforcement](#crdupgradesafetyenforcement)_ | enforcement is required and configures 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 is skipped during an upgrade operation.
Use this option with caution as unintended consequences such as data loss can occur.
When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. | | Enum: [None Strict]
Required: \{\}
| #### CatalogFilter @@ -96,11 +96,11 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `packageName` _string_ | 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
Required: \{\}
| -| `version` _string_ | 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
| -| `channels` _string array_ | 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 | | MaxItems: 256
items:MaxLength: 253
items:XValidation: \{self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") channels entries must be valid DNS1123 subdomains \}
| -| `selector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta)_ | 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. | | | -| `upgradeConstraintPolicy` _[UpgradeConstraintPolicy](#upgradeconstraintpolicy)_ | 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". | CatalogProvided | Enum: [CatalogProvided SelfCertified]
| +| `packageName` _string_ | packageName specifies the name of the package to be installed and is used to filter
the content from catalogs.
It 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
Required: \{\}
| +| `version` _string_ | version is an optional semver constraint (a specific version or range of versions).
When unspecified, the latest version available is 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.
You can add additional comparison strings 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
Optional: \{\}
| +| `channels` _string array_ | channels is optional and specifies 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.
You can specify no more than 256 channels.
When specified, it constrains 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 are considered installable
- Automatic upgrades are constrained to upgrade edges defined by the selected channel
When unspecified, upgrade edges across all channels are 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 | | MaxItems: 256
items:MaxLength: 253
items:XValidation: \{self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") channels entries must be valid DNS1123 subdomains \}
Optional: \{\}
| +| `selector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#labelselector-v1-meta)_ | selector is optional and filters the set of ClusterCatalogs used in the bundle selection process.
When unspecified, all ClusterCatalogs are used in the bundle selection process. | | Optional: \{\}
| +| `upgradeConstraintPolicy` _[UpgradeConstraintPolicy](#upgradeconstraintpolicy)_ | upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog
are enforced for the package referenced in the packageName field.
Allowed values are "CatalogProvided", "SelfCertified", or omitted.
When set to "CatalogProvided", automatic upgrades only occur when upgrade constraints specified by the package
author are met.
When set to "SelfCertified", the upgrade constraints specified by the package author are ignored.
This allows 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.
Use this option only if you have independently verified the changes.
When omitted, the default value is "CatalogProvided". | CatalogProvided | Enum: [CatalogProvided SelfCertified]
Optional: \{\}
| #### CatalogSource @@ -117,15 +117,15 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `type` _[SourceType](#sourcetype)_ | 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]
Required: \{\}
| -| `image` _[ImageSource](#imagesource)_ | 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. | | | +| `type` _[SourceType](#sourcetype)_ | type is a required field that specifies the type of source for the catalog.
The only allowed value is "Image".
When set to "Image", the ClusterCatalog content is 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]
Required: \{\}
| +| `image` _[ImageSource](#imagesource)_ | image configures how catalog contents are sourced from an OCI image.
It is required when type is Image, and forbidden otherwise. | | Optional: \{\}
| #### ClusterCatalog -ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. +ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs @@ -137,11 +137,11 @@ _Appears in:_ | --- | --- | --- | --- | | `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | | `kind` _string_ | `ClusterCatalog` | | | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | +| `kind` _string_ | 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 | | Optional: \{\}
| +| `apiVersion` _string_ | 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 | | Optional: \{\}
| | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | -| `spec` _[ClusterCatalogSpec](#clustercatalogspec)_ | 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. | | Required: \{\}
| -| `status` _[ClusterCatalogStatus](#clustercatalogstatus)_ | 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 | | | +| `spec` _[ClusterCatalogSpec](#clustercatalogspec)_ | spec is a required field that defines the desired state of the ClusterCatalog.
The controller ensures that the catalog is unpacked and served over the catalog content HTTP server. | | Required: \{\}
| +| `status` _[ClusterCatalogStatus](#clustercatalogstatus)_ | status contains the following information about the state of the ClusterCatalog:
- Whether the catalog contents are being served via the catalog content HTTP server
- Whether the ClusterCatalog is progressing to a new state
- A reference to the source from which the catalog contents were retrieved | | Optional: \{\}
| #### ClusterCatalogList @@ -158,8 +158,8 @@ ClusterCatalogList contains a list of ClusterCatalog | --- | --- | --- | --- | | `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | | `kind` _string_ | `ClusterCatalogList` | | | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | +| `kind` _string_ | 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 | | Optional: \{\}
| +| `apiVersion` _string_ | 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 | | Optional: \{\}
| | `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `items` _[ClusterCatalog](#clustercatalog) array_ | items is a list of ClusterCatalogs.
items is required. | | Required: \{\}
| @@ -177,9 +177,9 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `source` _[CatalogSource](#catalogsource)_ | 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 | | Required: \{\}
| -| `priority` _integer_ | 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. | 0 | | -| `availabilityMode` _[AvailabilityMode](#availabilitymode)_ | 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. | Available | Enum: [Unavailable Available]
| +| `source` _[CatalogSource](#catalogsource)_ | source is a required field that defines the source of a catalog.
A catalog contains information on content that can be installed on a cluster.
The catalog source makes catalog contents discoverable and usable by other on-cluster components.
These components can present the content in a GUI dashboard or install 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.
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 | | Required: \{\}
| +| `priority` _integer_ | priority is an optional field that defines a priority for this ClusterCatalog.
Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements.
Higher numbers mean higher priority.
Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements.
Clients should prompt users for additional input to break the tie.
When omitted, the default priority is 0.
Use negative numbers to specify a priority lower than the default.
Use positive numbers to specify a priority higher than the default.
The lowest possible value is -2147483648.
The highest possible value is 2147483647. | 0 | Maximum: 2.147483647e+09
Minimum: -2.147483648e+09
Optional: \{\}
| +| `availabilityMode` _[AvailabilityMode](#availabilitymode)_ | availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster.
Allowed values are "Available", "Unavailable", or omitted.
When omitted, the default value is "Available".
When set to "Available", the catalog contents are unpacked and served over the catalog content HTTP server.
Clients should consider this ClusterCatalog and its contents as usable.
When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server.
Treat this the same as if the ClusterCatalog does not exist.
Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. | Available | Enum: [Unavailable Available]
Optional: \{\}
| #### ClusterCatalogStatus @@ -195,10 +195,10 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | 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. | | | -| `resolvedSource` _[ResolvedCatalogSource](#resolvedcatalogsource)_ | resolvedSource contains information about the resolved source based on the source type. | | | -| `urls` _[ClusterCatalogURLs](#clustercatalogurls)_ | urls contains the URLs that can be used to access the catalog. | | | -| `lastUnpacked` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#time-v1-meta)_ | 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". | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions represents the current state of this ClusterCatalog.
The current condition types are Serving and Progressing.
The Serving condition represents whether the catalog contents are being served via the HTTP(S) web server:
- When status is True and reason is Available, the catalog contents are being served.
- When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available.
- When status is False and reason is UserSpecifiedUnavailable, the catalog contents are not being served because the catalog has been intentionally marked as unavailable.
The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state:
- When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts.
- When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing.
- When status is False and reason is Blocked, an error occurred that requires manual intervention for recovery.
If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously:
- The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server.
- The Progressing condition is True with reason Retrying because the system is working to serve the new version. | | Optional: \{\}
| +| `resolvedSource` _[ResolvedCatalogSource](#resolvedcatalogsource)_ | resolvedSource contains information about the resolved source based on the source type. | | Optional: \{\}
| +| `urls` _[ClusterCatalogURLs](#clustercatalogurls)_ | urls contains the URLs that can be used to access the catalog. | | Optional: \{\}
| +| `lastUnpacked` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#time-v1-meta)_ | lastUnpacked represents the last time the catalog contents were extracted from their source format.
For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache.
This extraction from the source format is called "unpacking". | | Optional: \{\}
| #### ClusterCatalogURLs @@ -214,7 +214,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `base` _string_ | 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
Required: \{\}
| +| `base` _string_ | base is a cluster-internal URL that provides endpoints for accessing the catalog content.
Clients should append the path for the endpoint they want 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 entire catalog contents in the FBC format
New endpoints may be added as needs evolve. | | MaxLength: 525
Required: \{\}
| #### ClusterExtension @@ -232,11 +232,11 @@ _Appears in:_ | --- | --- | --- | --- | | `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | | `kind` _string_ | `ClusterExtension` | | | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | -| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | -| `spec` _[ClusterExtensionSpec](#clusterextensionspec)_ | spec is an optional field that defines the desired state of the ClusterExtension. | | | -| `status` _[ClusterExtensionStatus](#clusterextensionstatus)_ | status is an optional field that defines the observed state of the ClusterExtension. | | | +| `kind` _string_ | 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 | | Optional: \{\}
| +| `apiVersion` _string_ | 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 | | Optional: \{\}
| +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| +| `spec` _[ClusterExtensionSpec](#clusterextensionspec)_ | spec is an optional field that defines the desired state of the ClusterExtension. | | Optional: \{\}
| +| `status` _[ClusterExtensionStatus](#clusterextensionstatus)_ | status is an optional field that defines the observed state of the ClusterExtension. | | Optional: \{\}
| #### ClusterExtensionConfig @@ -253,8 +253,8 @@ _Appears in:_ | 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 is used to specify arbitrary configuration values for the ClusterExtension.
It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
The configuration values are validated at runtime against a JSON schema provided by the bundle. | | MinProperties: 1
Type: object
| +| `configType` _[ClusterExtensionConfigType](#clusterextensionconfigtype)_ | configType is required and specifies the type of configuration source.
The only allowed value is "Inline".
When 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.
It is used to specify arbitrary configuration values for the ClusterExtension.
It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property.
The configuration values are validated at runtime against a JSON schema provided by the bundle. | | MinProperties: 1
Type: object
Optional: \{\}
| #### ClusterExtensionConfigType @@ -287,7 +287,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `preflight` _[PreflightConfig](#preflightconfig)_ | 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. | | | +| `preflight` _[PreflightConfig](#preflightconfig)_ | preflight is optional and configures the checks that 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 is used. | | Optional: \{\}
| #### ClusterExtensionInstallStatus @@ -303,7 +303,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `bundle` _[BundleMetadata](#bundlemetadata)_ | 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. | | Required: \{\}
| +| `bundle` _[BundleMetadata](#bundlemetadata)_ | bundle is required and 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. | | Required: \{\}
| #### ClusterExtensionList @@ -320,9 +320,9 @@ ClusterExtensionList contains a list of ClusterExtension | --- | --- | --- | --- | | `apiVersion` _string_ | `olm.operatorframework.io/v1` | | | | `kind` _string_ | `ClusterExtensionList` | | | -| `kind` _string_ | 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 | | | -| `apiVersion` _string_ | 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 | | | -| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `kind` _string_ | 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 | | Optional: \{\}
| +| `apiVersion` _string_ | 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 | | Optional: \{\}
| +| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| | `items` _[ClusterExtension](#clusterextension) array_ | items is a required list of ClusterExtension objects. | | Required: \{\}
| @@ -339,11 +339,12 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `namespace` _string_ | 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
Required: \{\}
| -| `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.
| | | +| `namespace` _string_ | namespace specifies a Kubernetes namespace.
This is the namespace where 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.
The namespace field 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
Required: \{\}
| +| `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount specifies 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.
The serviceAccount field is required. | | Required: \{\}
| +| `source` _[SourceConfig](#sourceconfig)_ | source is required and selects the installation source of content for this ClusterExtension.
Set the sourceType field to perform the selection.
Catalog is currently the only implemented sourceType.
Setting 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 optional and configures installation options for the ClusterExtension,
such as the pre-flight check configuration. | | Optional: \{\}
| +| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is optional and specifies bundle-specific configuration.
Configuration is bundle-specific and a bundle may provide a configuration schema.
When not specified, the default configuration of the resolved bundle is used.
config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
a configuration schema the bundle is deemed to not be configurable. More information on how
to configure bundles can be found in the OLM documentation associated with your current OLM version. | | Optional: \{\}
| +| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
| | Maximum: 720
Minimum: 10
Optional: \{\}
| #### ClusterExtensionStatus @@ -359,9 +360,9 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | 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 Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.

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. | | | -| `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | | -| `activeRevisions` _[RevisionStatus](#revisionstatus) array_ | activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
including both installed and rolling out revisions.
| | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions represents the current state of the ClusterExtension.
The set of condition types which apply to all spec.source variations are Installed and Progressing.
The Installed condition represents whether 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 Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out.

When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata.
These are indications from a package owner to guide users away from a particular package, channel, or bundle:
- BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable.
- ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable.
- PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable.
- Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. | | Optional: \{\}
| +| `install` _[ClusterExtensionInstallStatus](#clusterextensioninstallstatus)_ | install is a representation of the current installation status for this ClusterExtension. | | Optional: \{\}
| +| `activeRevisions` _[RevisionStatus](#revisionstatus) array_ | activeRevisions holds a list of currently active (non-archived) ClusterExtensionRevisions,
including both installed and rolling out revisions.
| | Optional: \{\}
| @@ -382,8 +383,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `ref` _string_ | 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
Required: \{\}
| -| `pollIntervalMinutes` _integer_ | 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
| +| `ref` _string_ | ref is a required field that defines the reference to a container image containing catalog contents.
It cannot be more than 1000 characters.
A reference has 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
Required: \{\}
| +| `pollIntervalMinutes` _integer_ | pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content.
You cannot specify pollIntervalMinutes when ref is a digest-based reference.
When omitted, the image is not polled for new content. | | Minimum: 1
Optional: \{\}
| #### PreflightConfig @@ -399,7 +400,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `crdUpgradeSafety` _[CRDUpgradeSafetyPreflightConfig](#crdupgradesafetypreflightconfig)_ | 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](#crdupgradesafetypreflightconfig)_ | crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run
before upgrades of installed content.
The CRD Upgrade Safety pre-flight check safeguards from unintended consequences of upgrading a CRD,
such as data loss. | | | #### ResolvedCatalogSource @@ -416,8 +417,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `type` _[SourceType](#sourcetype)_ | 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]
Required: \{\}
| -| `image` _[ResolvedImageSource](#resolvedimagesource)_ | 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. | | | +| `type` _[SourceType](#sourcetype)_ | type is a required field that specifies the type of source for the catalog.
The only allowed value is "Image".
When set to "Image", information about the resolved image source is set in the image field. | | Enum: [Image]
Required: \{\}
| +| `image` _[ResolvedImageSource](#resolvedimagesource)_ | image contains resolution information for a catalog sourced from an image.
It must be set when type is Image, and forbidden otherwise. | | | #### ResolvedImageSource @@ -433,7 +434,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `ref` _string_ | 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
Required: \{\}
| +| `ref` _string_ | ref contains the resolved image digest-based reference.
The digest format allows you to use other tooling to fetch the exact OCI manifests
that were used to extract the catalog contents. | | MaxLength: 1000
Required: \{\}
| #### RevisionStatus @@ -450,7 +451,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | name of the ClusterExtensionRevision resource | | | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions optionally expose Progressing and Available condition of the revision,
in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
to get more insights about reasons for its current state. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#condition-v1-meta) array_ | conditions optionally expose Progressing and Available condition of the revision,
in case when it is not yet marked as successfully installed (condition Succeeded is not set to True).
Given that a ClusterExtension should remain available during upgrades, an observer may use these conditions
to get more insights about reasons for its current state. | | Optional: \{\}
| #### ServiceAccountReference @@ -466,7 +467,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | 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
Required: \{\}
| +| `name` _string_ | name is a required, immutable reference to the name of the ServiceAccount used for installation
and management of the content for the package specified in the packageName field.
This ServiceAccount must exist in the installNamespace.
The name field 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
Required: \{\}
| #### SourceConfig @@ -482,8 +483,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `sourceType` _string_ | 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]
Required: \{\}
| -| `catalog` _[CatalogFilter](#catalogfilter)_ | catalog is used to configure how information is sourced from a catalog.
This field is required when sourceType is "Catalog", and forbidden otherwise. | | | +| `sourceType` _string_ | sourceType is required and specifies the type of install source.
The only allowed value is "Catalog".
When set to "Catalog", information for determining the appropriate bundle of content to install
is fetched from ClusterCatalog resources on the cluster.
When using the Catalog sourceType, the catalog field must also be set. | | Enum: [Catalog]
Required: \{\}
| +| `catalog` _[CatalogFilter](#catalogfilter)_ | catalog configures how information is sourced from a catalog.
It is required when sourceType is "Catalog", and forbidden otherwise. | | Optional: \{\}
| #### SourceType diff --git a/docs/draft/howto/configure-bundles.md b/docs/draft/howto/configure-bundles.md new file mode 100644 index 0000000000..3db23a4f2b --- /dev/null +++ b/docs/draft/howto/configure-bundles.md @@ -0,0 +1,172 @@ +## Description + +!!! note +This feature is still in `alpha` and the `SingleOwnNamespaceInstallSupport` feature-gate must be enabled to make use of it. +See the instructions below on how to enable it. + +--- + +# Configuring OLM v1 Extensions: Migration and Reference + +In OLM v1, the way extensions are configured has changed significantly to improve flexibility and consistency. This guide explains the architectural shift from OLM v0, how to inspect bundles for supported configurations, and how to correctly configure `registry+v1` (legacy) bundles using the new `ClusterExtension` API. + +## OLM v0 vs. OLM v1: The Configuration Shift + +In **OLM v0**, configuration was split across multiple resources and concepts. "Install Modes" (defining which namespaces an Operator watches) were handled by the `OperatorGroup` resource, while operand configuration (environment variables, resource limits) was handled via the `Subscription` resource. + +In **OLM v1**, these concepts are unified under the **ClusterExtension** resource. + +| Feature | OLM v0 Approach | OLM v1 Approach | +|:--------------------|:----------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------| +| **Namespace Scope** | Defined by **OperatorGroup**. You had to pre-create an OperatorGroup to tell the Operator where to watch. | Defined by **Configuration**. You provide a `watchNamespace` value directly in the `ClusterExtension` YAML. | +| **User Settings** | `Subscription.spec.config` (e.g., env, resources). | `ClusterExtension.spec.config.inline` (arbitrary JSON/YAML defined by the bundle author). | +| **Multi-Tenancy** | "Install Modes" allowed multiple instances of an operator to exist. | "Install Modes" are treated as **bundle configuration**. You install the extension once, and configure it to watch specific areas. | + +### The `watchNamespace` Configuration + +For existing `registry+v1` bundles (standard OLM bundles), OLM v1 automatically generates a configuration schema based on the bundle's capabilities. The primary configuration field available is `watchNamespace`. + +* **OLM v0:** You selected `SingleNamespace` mode by creating an `OperatorGroup` that targeted a specific namespace. +* **OLM v1:** You set `watchNamespace: "my-target-namespace"` inside the `ClusterExtension` config. + +## Step 1: Identifying Bundle Capabilities + +Before configuring a bundle, you must understand which Install Modes it supports. OLM v1 does not allow you to force a configuration that the bundle author has not explicitly supported. + +You can inspect a bundle image using the `opm` CLI tool and `jq` to parse the output. + +**Prerequisites:** +* `opm` CLI installed. +* `jq` installed. + +**Command:** + +Run the following command, replacing `` with your target image (e.g., `quay.io/example/my-operator-bundle:v1.0.0`). + +```bash +opm render -o json | \ +jq 'select(.schema == "olm.bundle") | .properties[] | select(.type == "olm.csv") | .value.spec.installModes' +``` + +**Example Output:** + +```json +[ + { + "type": "OwnNamespace", + "supported": true + }, + { + "type": "SingleNamespace", + "supported": true + }, + { + "type": "MultiNamespace", + "supported": false + }, + { + "type": "AllNamespaces", + "supported": false + } +] +``` + +By analyzing which modes are marked `true`, you can determine how to configure the `ClusterExtension` in the next step. + +## Step 2: Capability Matrix & Configuration Guide + +Use the output from Step 1 to locate your bundle's capabilities in the matrix below. This determines if you *must* provide configuration, if it is optional, and what values are valid. + +### Legend +* **Install Namespace:** The namespace where the Operator logic (Pod) runs (defined in `ClusterExtension.spec.namespace`). +* **Watch Namespace:** The namespace the Operator monitors for Custom Resources (defined in `spec.config.inline.watchNamespace`). + +### Configuration Matrix + +| Capabilities Detected (from `opm`) | `watchNamespace` Field Status | Valid Values / Constraints | +|:-----------------------------------------|:------------------------------|:----------------------------------------------------------------------------------------------------------| +| **OwnNamespace ONLY** | **Required** | Must be exactly the same as the **Install Namespace**. | +| **SingleNamespace ONLY** | **Required** | Must be **different** from the Install Namespace. | +| **OwnNamespace** AND **SingleNamespace** | **Required** | Can be **any** namespace (either the install namespace or a different one). | +| **AllNamespaces** (regardless of others) | **Optional** | If omitted: defaults to Cluster-wide watch.
If provided: can be any specific namespace (limits scope). | + +### Common Configuration Scenarios + +#### Scenario A: The Legacy "OwnNamespace" Operator +* **Capability:** Only supports `OwnNamespace`. +* **Requirement:** The operator is hardcoded to watch its own namespace. +* **Config:** You must explicitly set `watchNamespace` to match the installation namespace. + +```yaml +apiVersion: olm.operatorframework.io/v1 +kind: ClusterExtension +metadata: + name: my-operator +spec: + namespace: my-operator-ns # <--- Install Namespace + serviceAccount: + name: my-sa + config: + configType: Inline + inline: + watchNamespace: my-operator-ns # <--- MUST match Install Namespace + source: + sourceType: Catalog + catalog: + packageName: my-package +``` + +#### Scenario B: The "SingleNamespace" Operator +* **Capability:** Supports `SingleNamespace` (but not `OwnNamespace`). +* **Requirement:** The operator runs in one namespace (e.g., `ops-system`) but watches workloads in another (e.g., `dev-team-a`). +* **Config:** You must set `watchNamespace` to the target workload namespace. + +```yaml +apiVersion: olm.operatorframework.io/v1 +kind: ClusterExtension +metadata: + name: monitor-operator +spec: + namespace: ops-system # <--- Install Namespace + serviceAccount: + name: monitor-operator-installer + source: + sourceType: Catalog + catalog: + packageName: monitor-operator + config: + configType: Inline + inline: + watchNamespace: dev-team-a # <--- MUST differ from Install Namespace +``` + +#### Scenario C: The Modern "AllNamespaces" Operator +* **Capability:** Only supports `AllNamespaces`. + +```yaml +apiVersion: olm.operatorframework.io/v1 +kind: ClusterExtension +metadata: + name: global-operator +spec: + namespace: operators + # No config provided = Operator watches the entire cluster (AllNamespaces) + serviceAccount: + name: global-operator-installer + source: + sourceType: Catalog + catalog: + packageName: global-operator +``` + + +## Troubleshooting Configuration Errors + +OLM v1 validates your configuration against the bundle's schema before installation proceeds. If your configuration is invalid, the `ClusterExtension` will report a `Progressing` condition with an error message. + +| Error Message Example | Cause | Solution | +|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| `required field "watchNamespace" is missing` | The bundle does not support `AllNamespaces` default mode. | Add the `inline` block and specify a `watchNamespace`. | +| `invalid value "X": watchNamespace must be "Y" (the namespace where the operator is installed) because this operator only supports OwnNamespace install mode` | You tried to set a different watch namespace for an `OwnNamespace`-only bundle. | Change `watchNamespace` to match `spec.namespace`. | +| `invalid value "X": watchNamespace must be different from "Y" (the install namespace) because this operator uses SingleNamespace install mode to watch a different namespace` | You tried to set the watch namespace to the install namespace for a `SingleNamespace`-only bundle. | Change `watchNamespace` to a different target namespace. | +| `unknown field "foo"` | You added extra fields to the inline config. | Remove fields other than `watchNamespace` (unless the bundle author explicitly documents extra schema support). | diff --git a/docs/draft/howto/single-ownnamespace-install.md b/docs/draft/howto/single-ownnamespace-install.md index 8e6f00fe49..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,28 +30,6 @@ include *installModes*. [![OwnNamespace Install Demo](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i.svg)](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i) -## Enabling the Feature-Gate - -!!! tip - -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. - ---- - -Patch the `operator-controller` `Deployment` adding `--feature-gates=SingleOwnNamespaceInstallSupport=true` to the -controller container arguments: - -```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"}]' -``` - -Wait for `Deployment` rollout: - -```terminal title="Wait for Deployment rollout" -kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager -``` - ## Configuring the `ClusterExtension` A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the diff --git a/docs/draft/tutorials/explore-available-content-metas-endpoint.md b/docs/draft/tutorials/explore-available-content-metas-endpoint.md index f17271d3e4..5d04b02df1 100644 --- a/docs/draft/tutorials/explore-available-content-metas-endpoint.md +++ b/docs/draft/tutorials/explore-available-content-metas-endpoint.md @@ -91,9 +91,6 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ... ``` - !!! important - OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate. - 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 diff --git a/docs/project/olmv1_limitations.md b/docs/project/olmv1_limitations.md index 01ce9436d3..8f0e921e34 100644 --- a/docs/project/olmv1_limitations.md +++ b/docs/project/olmv1_limitations.md @@ -8,7 +8,8 @@ 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** support installation via the `AllNamespaces` install mode. + * Note that `AllNamespaces` is the recommended install mode. OLM v1 supports `SingleNamespace` and `OwnNamespace` modes for `registry+v1` bundles for backwards compatibility with OLM v0, but these are not recommended install modes as there is a [hard limitation](https://operator-framework.github.io/operator-controller/project/olmv1_design_decisions/#do-not-fight-kubernetes) of only one instance of a given CRD in the cluster. * **must not** declare dependencies using any of the following file-based catalog properties: * `olm.gvk.required` * `olm.package.required` diff --git a/docs/tutorials/explore-available-content.md b/docs/tutorials/explore-available-content.md index 36e3cf8834..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 - OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate. - 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 89aa72d7b8..9d95d39217 100644 --- a/go.mod +++ b/go.mod @@ -1,62 +1,64 @@ module github.com/operator-framework/operator-controller -go 1.24.6 +go 1.25.3 require ( - github.com/BurntSushi/toml v1.5.0 + github.com/BurntSushi/toml v1.6.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/blang/semver/v4 v4.0.0 - github.com/cert-manager/cert-manager v1.18.2 - github.com/containerd/containerd v1.7.29 + github.com/cert-manager/cert-manager v1.19.3 + github.com/containerd/containerd v1.7.30 + github.com/cucumber/godog v0.15.1 + github.com/evanphx/json-patch v5.9.11+incompatible github.com/fsnotify/fsnotify v1.9.0 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.7 - github.com/google/renameio/v2 v2.0.1 + github.com/google/renameio/v2 v2.0.2 github.com/gorilla/handlers v1.5.2 - github.com/klauspost/compress v1.18.1 + github.com/klauspost/compress v1.18.3 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 - github.com/operator-framework/api v0.36.0 + github.com/operator-framework/api v0.38.0 github.com/operator-framework/helm-operator-plugins v0.8.0 - github.com/operator-framework/operator-registry v1.61.0 + github.com/operator-framework/operator-registry v1.62.0 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.4 + github.com/prometheus/common v0.67.5 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 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.30.0 - golang.org/x/sync v0.18.0 - golang.org/x/tools v0.39.0 - helm.sh/helm/v3 v3.19.3 - 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 + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 + golang.org/x/mod v0.32.0 + golang.org/x/sync v0.19.0 + golang.org/x/tools v0.41.0 + helm.sh/helm/v3 v3.20.0 + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/apiserver v0.35.0 + k8s.io/cli-runtime v0.35.0 + k8s.io/client-go v0.35.0 + k8s.io/component-base v0.35.0 k8s.io/klog/v2 v2.130.1 k8s.io/kubernetes v1.34.0 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - pkg.package-operator.run/boxcutter v0.7.1 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + pkg.package-operator.run/boxcutter v0.9.0 sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/controller-tools v0.19.0 + sigs.k8s.io/controller-tools v0.20.0 sigs.k8s.io/crdify v0.5.0 sigs.k8s.io/yaml v1.6.0 ) require ( k8s.io/component-helpers v0.34.0 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // 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 @@ -71,7 +73,7 @@ require ( 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/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // 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 @@ -86,18 +88,19 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.1 // indirect + github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect + github.com/cucumber/messages/go/v21 v21.0.1 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect - github.com/cyphar/filepath-securejoin v0.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.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 v29.0.3+incompatible // indirect + github.com/docker/cli v29.1.5+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+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.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.18.0 // indirect @@ -106,61 +109,63 @@ require ( 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.2 // indirect - github.com/go-git/go-git/v5 v5.16.2 // indirect + github.com/go-git/go-git/v5 v5.16.5 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/stdr v1.2.2 // 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/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/gofrs/uuid v4.3.1+incompatible // 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.26.1 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // 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.27.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // 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 + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.4 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/joelanford/ignore v0.1.1 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect 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-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-colorable v0.1.14 // 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.32 // indirect + github.com/mattn/go-sqlite3 v1.14.33 // 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 @@ -179,7 +184,8 @@ require ( 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/onsi/gomega v1.38.2 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/onsi/gomega v1.39.0 // 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 @@ -189,22 +195,21 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/prometheus/procfs v0.19.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // 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.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/sigstore/fulcio v1.8.5 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/sigstore v1.10.4 // indirect + github.com/sirupsen/logrus v1.9.4 // 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.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/vbauerster/mpb/v8 v8.10.2 // indirect @@ -212,47 +217,47 @@ require ( github.com/xlab/treeprint v1.2.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.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.66.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.podman.io/common v0.66.1 // 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.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.13.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.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-20250804133106-a7a43d27e69b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // 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.33.2 // indirect - k8s.io/kubectl v0.34.0 // indirect + k8s.io/kubectl v0.35.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/gateway-api v1.4.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // 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 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) retract v1.5.0 // contains filename with ':' which causes failure creating module zip file diff --git a/go.sum b/go.sum index c9879d9aa8..31c71f2639 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= @@ -10,8 +10,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h 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= -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/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -44,11 +44,11 @@ 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/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/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.18.2 h1:H2P75ycGcTMauV3gvpkDqLdS3RSXonWF2S49QGA1PZE= -github.com/cert-manager/cert-manager v1.18.2/go.mod h1:icDJx4kG9BCNpGjBvrmsFd99d+lXUvWdkkcrSSQdIiw= +github.com/cert-manager/cert-manager v1.19.3 h1:3d0Nk/HO3BOmAdBJNaBh+6YgaO3Ciey3xCpOjiX5Obs= +github.com/cert-manager/cert-manager v1.19.3/go.mod h1:e9NzLtOKxTw7y99qLyWGmPo6mrC1Nh0EKKcMkRfK+GE= 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= @@ -57,8 +57,8 @@ 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.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= -github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= +github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= 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= @@ -85,13 +85,21 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 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.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= -github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= 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= @@ -104,8 +112,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 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 v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= -github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao= +github.com/docker/cli v29.1.5+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.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= @@ -136,10 +144,11 @@ 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= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -150,8 +159,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= -github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= 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.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -163,58 +172,65 @@ 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/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-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 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/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= 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= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -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/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/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= @@ -230,8 +246,8 @@ 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.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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.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= @@ -248,8 +264,8 @@ 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-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -github.com/google/renameio/v2 v2.0.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0= -github.com/google/renameio/v2 v2.0.1/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= +github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw= +github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo= 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= @@ -265,8 +281,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr 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.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZt8+kYaqUisuuIGTk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= 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= @@ -274,8 +290,19 @@ github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8 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-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= 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-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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= @@ -286,26 +313,25 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= -github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joelanford/ignore v0.1.1 h1:vKky5RDoPT+WbONrbQBgOn95VV/UPh4ejlyAbbzgnQk= 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/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/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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -314,26 +340,21 @@ 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-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= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -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/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -378,24 +399,24 @@ 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.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -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/onsi/ginkgo/v2 v2.27.5 h1:ZeVgZMx2PDMdJm/+w5fE/OyG6ILo1Y3e+QX4zSR0zTE= +github.com/onsi/ginkgo/v2 v2.27.5/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= +github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 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/operator-framework/api v0.36.0 h1:6+duRhamCvB540JbvNp/1+Pot7luff7HqdAOm9bAntg= -github.com/operator-framework/api v0.36.0/go.mod h1:QSmHMx8XpGsNWvjU5CUelVZC916VLp/TZhfYvGKpghM= +github.com/operator-framework/api v0.38.0 h1:RbIhBH7pot/tlVPkEppXOh2zT7GpSUjPJoC8pRnNnBo= +github.com/operator-framework/api v0.38.0/go.mod h1:6UCZhZPh9zAZZq1D9B2+IO0ibVwHdTiNYLYRr8ZT8Mk= 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.61.0 h1:LgX6lP5hUHfpMTMygsnySc7PKxibzqIoqWUm6NPWl2M= -github.com/operator-framework/operator-registry v1.61.0/go.mod h1:KkZG1G7O/qz0J7d9Z0ukV9sgag7aRQ5xwcFRHUyn8Uc= +github.com/operator-framework/operator-registry v1.62.0 h1:9BwFNU/NWfFAr8nZ0Lmcvmztnd4XXbQLdzrvLqrv+M0= +github.com/operator-framework/operator-registry v1.62.0/go.mod h1:NsgBzgp1nuM1lZ7IyEe2FsJYadY6EHj5pL3SxLokvbQ= 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= @@ -418,10 +439,10 @@ github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 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.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -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/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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= @@ -433,8 +454,8 @@ 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.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/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= 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.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -445,20 +466,23 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN 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.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/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= +github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= +github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= 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/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -469,8 +493,8 @@ github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w 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/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/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= 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= @@ -478,10 +502,9 @@ 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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.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.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= @@ -505,18 +528,18 @@ 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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= 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= @@ -525,10 +548,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwd 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 v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= 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= @@ -541,20 +564,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwW 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/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 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.66.0 h1:KElE3HKLFdMdJL+jv5ExBiX2Dh4Qcv8ovmzaBGRsyZM= -go.podman.io/common v0.66.0/go.mod h1:aNd2a0S7pY+fx1X5kpQYuF4hbwLU8ZOccuVrhu7h1Xc= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.podman.io/common v0.66.1 h1:zDyd4HhVgQAN8LupBHCnhtM3FEOJ9DwmThjulXZq2qA= +go.podman.io/common v0.66.1/go.mod h1:aNd2a0S7pY+fx1X5kpQYuF4hbwLU8ZOccuVrhu7h1Xc= 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= @@ -563,8 +586,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.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= @@ -577,11 +600,11 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -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/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= 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= @@ -592,8 +615,8 @@ 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -610,11 +633,11 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -626,8 +649,8 @@ 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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -636,9 +659,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -646,8 +668,8 @@ 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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.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= @@ -657,8 +679,8 @@ 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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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= @@ -668,10 +690,10 @@ 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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.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= @@ -684,10 +706,10 @@ 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -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 v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-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= @@ -703,19 +725,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 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-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-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 h1:tK4fkUnnRhig9TsTp4otV1FxwBFYgbKUq1RY0V6KZ4U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= +google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 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.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 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= @@ -725,8 +747,8 @@ 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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= @@ -745,8 +767,8 @@ 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.19.3 h1:cTOsZ7XfjD9c05mPKTC1FjRT4h2cKzszfD5aSa72GM8= -helm.sh/helm/v3 v3.19.3/go.mod h1:vup/q0mmu4G+YD2xr9qF5GhhWdoj+wm2gXWojk5jnks= +helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= +helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= 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.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= @@ -769,37 +791,37 @@ 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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 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= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/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= -pkg.package-operator.run/boxcutter v0.7.1 h1:us5wn0px9aAkumrXiQx38+Sc9dTgKJsHFbePoRQeWRo= -pkg.package-operator.run/boxcutter v0.7.1/go.mod h1:xEOKM3e3xtfSKYIOssQnu6DOWceiIu52qziMDcmUmpI= +pkg.package-operator.run/boxcutter v0.9.0 h1:uBypecAVb178RG5dL3Z3T85Ou/KSL0/4BiQTcHRTnqY= +pkg.package-operator.run/boxcutter v0.9.0/go.mod h1:nk6XIcTS3i8WV1+GpaFwjPcNM+tK0jHczoa0qGnbUbk= 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.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= -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/controller-tools v0.20.0 h1:VWZF71pwSQ2lZZCt7hFGJsOfDc5dVG28/IysjjMWXL8= +sigs.k8s.io/controller-tools v0.20.0/go.mod h1:b4qPmjGU3iZwqn34alUU5tILhNa9+VXK+J3QV0fT/uU= 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/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ= +sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/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/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/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/api-lint-diff/run.sh b/hack/api-lint-diff/run.sh new file mode 100755 index 0000000000..04f245b7d2 --- /dev/null +++ b/hack/api-lint-diff/run.sh @@ -0,0 +1,557 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Temporary directory for this run +TEMP_DIR="" +WORKTREE_DIR="" +BASELINE_BRANCH="${BASELINE_BRANCH:-main}" +API_DIR="api" + +# Cleanup function +cleanup() { + # Clean up git worktree first if it exists + if [[ -n "${WORKTREE_DIR}" && -d "${WORKTREE_DIR}" ]]; then + git worktree remove "${WORKTREE_DIR}" --force &> /dev/null || true + fi + + # Clean up temporary directory + if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then + rm -rf "${TEMP_DIR}" + fi +} + +trap cleanup EXIT + +# Ensure we're in the repository root +if [[ ! -d ".git" ]]; then + echo -e "${RED}Error: Must be run from repository root${NC}" + exit 1 +fi + +if [[ ! -d "${API_DIR}" ]]; then + echo -e "${RED}Error: ${API_DIR}/ directory not found${NC}" + exit 1 +fi + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +echo -e "${BLUE}Using temporary directory: ${TEMP_DIR}${NC}" >&2 + +# Get golangci-lint version from bingo +get_golangci_version() { + # Extract version from .bingo/Variables.mk + # Format: GOLANGCI_LINT := $(GOBIN)/golangci-lint-v2.7.2 + local version + version=$(grep 'GOLANGCI_LINT :=' .bingo/Variables.mk 2>/dev/null | sed -E 's/.*golangci-lint-(v[0-9.]+).*/\1/') + + # Validate that we got a version + if [[ -z "${version}" ]]; then + echo -e "${YELLOW}Warning: Could not extract golangci-lint version from .bingo/Variables.mk${NC}" >&2 + echo -e "${YELLOW}Using default version: latest${NC}" >&2 + version="latest" + fi + + echo "${version}" +} + +# Create temporary golangci-lint config for kube-api-linter +# This config focuses only on kube-api-linter for the api/ directory +create_temp_config() { + cat > "${TEMP_DIR}/.golangci.yaml" <<'EOF' +version: "2" +output: + formats: + tab: + path: stdout + colors: false +linters: + enable: + - kubeapilinter + settings: + custom: + kubeapilinter: + type: module + description: "Kube API Linter plugin" + original-url: "sigs.k8s.io/kube-api-linter" + settings: + linters: {} + lintersConfig: + optionalfields: + pointers: + preference: WhenRequired +run: + timeout: 5m + +issues: + exclude-rules: + # Ignore generated files + - path: zz_generated\..*\.go + linters: + - kubeapilinter + max-issues-per-linter: 0 + max-same-issues: 0 +EOF +} + +# Get kube-api-linter version - pinned for supply chain security +get_kube_api_linter_version() { + # Pin to specific pseudo-version to avoid supply chain risks + # kube-api-linter doesn't have semantic version tags, so we use a pseudo-version + # Update this version intentionally as part of dependency management + # To update: GOPROXY=https://proxy.golang.org go list -m -json sigs.k8s.io/kube-api-linter@latest + local version="v0.0.0-20251219161032-180d2bd496ef" # Latest as of 2025-12-19 + + echo "${version}" +} + +# Create custom golangci-lint configuration +create_custom_gcl_config() { + # Get golangci-lint version from bingo + local golangci_version + golangci_version=$(get_golangci_version) + + # Validate version is not empty + if [[ -z "${golangci_version}" ]]; then + echo -e "${RED}Error: Failed to determine golangci-lint version from .bingo/Variables.mk${NC}" >&2 + exit 1 + fi + + # Get kube-api-linter version (pinned for supply chain security) + local kube_api_linter_version + kube_api_linter_version=$(get_kube_api_linter_version) + + # Create custom-gcl config + cat > "${TEMP_DIR}/.custom-gcl.yml" <&2 + + # Create custom config + create_custom_gcl_config + + # Build custom golangci-lint using the 'custom' command + # This requires the base golangci-lint binary and runs from TEMP_DIR + # where .custom-gcl.yml is located + echo -e "${BLUE}Running golangci-lint custom build...${NC}" >&2 + local build_output + local abs_base_linter + # Convert to absolute path + if [[ "${base_linter}" != /* ]]; then + abs_base_linter="$(pwd)/${base_linter}" + else + abs_base_linter="${base_linter}" + fi + + if ! build_output=$(cd "${TEMP_DIR}" && "${abs_base_linter}" custom -v 2>&1); then + echo -e "${YELLOW}Warning: Failed to build custom golangci-lint${NC}" >&2 + echo -e "${YELLOW}Build output:${NC}" >&2 + echo "${build_output}" >&2 + echo -e "${YELLOW}Falling back to base linter (kube-api-linter will not be available)${NC}" >&2 + return 1 + fi + echo -e "${BLUE}Custom linter build completed${NC}" >&2 + + if [[ -f "${custom_binary}" ]]; then + echo -e "${GREEN}Successfully built custom golangci-lint at ${custom_binary}${NC}" >&2 + # Only echo the binary path to stdout for capture + echo "${custom_binary}" + return 0 + else + echo -e "${YELLOW}Warning: Custom binary not found at expected location${NC}" >&2 + return 1 + fi +} + +# Function to check if golangci-lint has kube-api-linter +check_linter_support() { + local linter_path="$1" + if ! "${linter_path}" linters 2>/dev/null | grep -q "kubeapilinter"; then + echo -e "${YELLOW}Warning: golangci-lint at ${linter_path} does not have kubeapilinter plugin${NC}" + echo -e "${YELLOW}Linting results may be incomplete. Consider using custom golangci-lint build.${NC}" + return 1 + fi + return 0 +} + +# Find golangci-lint binary +find_golangci_lint() { + # Check if variables.env exists and extract golangci-lint path + if [[ -f ".bingo/variables.env" ]]; then + source .bingo/variables.env 2>/dev/null || true + if [[ -n "${GOLANGCI_LINT}" && -f "${GOLANGCI_LINT}" ]]; then + echo "${GOLANGCI_LINT}" + return 0 + fi + fi + + # Check for custom build + if [[ -f ".bingo/golangci-lint" ]]; then + echo ".bingo/golangci-lint" + return 0 + fi + + # Check for bin/golangci-lint + if [[ -f "bin/golangci-lint" ]]; then + echo "bin/golangci-lint" + return 0 + fi + + # Fall back to system golangci-lint + if command -v golangci-lint &> /dev/null; then + echo "golangci-lint" + return 0 + fi + + echo -e "${RED}Error: golangci-lint not found.${NC}" >&2 + echo -e "${RED}Searched for:${NC}" >&2 + echo -e " - .bingo/variables.env (bingo-managed variables for GOLANGCI_LINT)" >&2 + echo -e " - .bingo/golangci-lint" >&2 + echo -e " - bin/golangci-lint" >&2 + echo -e " - golangci-lint on your \$PATH" >&2 + exit 1 +} + +# Run linter and capture output +run_linter() { + local config_file="$1" + local output_file="$2" + local linter_path="$3" + local repo_root="${4:-$(pwd)}" + + # Run golangci-lint on api/ directory only + # Use absolute paths to ensure consistency + (cd "${repo_root}" && "${linter_path}" run \ + --config="${config_file}" \ + --path-prefix="" \ + ./api/...) > "${output_file}" 2>&1 || true +} + +# Parse linter output into structured format +# Format: filename:line:column:linter:message +parse_linter_output() { + local output_file="$1" + local parsed_file="$2" + + # Expected format: path/api/v1/file.go:123:45 linter message + # We need to: extract api/ relative path, parse line:col, linter, and message + grep "/${API_DIR}/" "${output_file}" | \ + sed -E "s|^.*/("${API_DIR}"/[^:]+):([0-9]+):([0-9]+)[[:space:]]+([^[:space:]]+)[[:space:]]+(.+)$|\1:\2:\3:\4:\5|" \ + > "${parsed_file}" || true +} + +# Get list of files changed in api/ directory compared to baseline +get_changed_files() { + git diff "${BASELINE_BRANCH}...HEAD" --name-only -- "${API_DIR}/" | \ + grep '\.go$' | \ + grep -v 'zz_generated' || true +} + +# Categorize issues as NEW, PRE-EXISTING, or FIXED +categorize_issues() { + local current_file="$1" + local baseline_file="$2" + local changed_files_file="$3" + local new_issues_file="$4" + local preexisting_issues_file="$5" + local fixed_issues_file="$6" + + # Read changed files into array + local changed_files=() + if [[ -f "${changed_files_file}" ]]; then + while IFS= read -r file; do + changed_files+=("${file}") + done < "${changed_files_file}" + fi + + # Process current issues only if file exists and is not empty + if [[ -f "${current_file}" && -s "${current_file}" ]]; then + while IFS= read -r line; do + [[ -z "${line}" ]] && continue + + local file + file=$(echo "${line}" | cut -d: -f1) + + # If no files were changed, all issues are pre-existing + if [[ ${#changed_files[@]} -eq 0 ]]; then + echo "${line}" >> "${preexisting_issues_file}" + continue + fi + + # Check if file was changed + local file_changed=false + for changed_file in "${changed_files[@]}"; do + if [[ "${file}" == "${changed_file}" ]]; then + file_changed=true + break + fi + done + + # If file wasn't changed, it's pre-existing + if ! $file_changed; then + echo "${line}" >> "${preexisting_issues_file}" + continue + fi + + # Check if issue exists in baseline + # Compare without line numbers since line numbers can change when code is added/removed + # Format is: file:line:col:linter:message + # We'll compare: file:linter:message + # Extract file (field 1), linter (field 4), and message (field 5+) from current issue + local file_linter_msg + file_linter_msg=$(echo "${line}" | cut -d: -f1,4,5-) + + # Check if baseline has a matching issue (same file, linter, message but possibly different line number) + # We need to extract the same fields from baseline and compare + local found=false + if [[ -f "${baseline_file}" ]]; then + while IFS= read -r baseline_line; do + [[ -z "${baseline_line}" ]] && continue + local baseline_file_linter_msg + baseline_file_linter_msg=$(echo "${baseline_line}" | cut -d: -f1,4,5-) + if [[ "${file_linter_msg}" == "${baseline_file_linter_msg}" ]]; then + found=true + break + fi + done < "${baseline_file}" + fi + + if $found; then + echo "${line}" >> "${preexisting_issues_file}" + else + echo "${line}" >> "${new_issues_file}" + fi + done < "${current_file}" + fi + + # Find FIXED issues - issues in baseline that are NOT in current + if [[ -f "${baseline_file}" && -s "${baseline_file}" ]]; then + while IFS= read -r baseline_line; do + [[ -z "${baseline_line}" ]] && continue + + local file + file=$(echo "${baseline_line}" | cut -d: -f1) + + # Only check files that were changed + if [[ ${#changed_files[@]} -gt 0 ]]; then + local file_changed=false + for changed_file in "${changed_files[@]}"; do + if [[ "${file}" == "${changed_file}" ]]; then + file_changed=true + break + fi + done + + # Skip if file wasn't changed + if ! $file_changed; then + continue + fi + fi + + # Extract file:linter:message from baseline + local baseline_file_linter_msg + baseline_file_linter_msg=$(echo "${baseline_line}" | cut -d: -f1,4,5-) + + # Check if this issue still exists in current + local still_exists=false + if [[ -f "${current_file}" ]]; then + while IFS= read -r current_line; do + [[ -z "${current_line}" ]] && continue + local current_file_linter_msg + current_file_linter_msg=$(echo "${current_line}" | cut -d: -f1,4,5-) + if [[ "${baseline_file_linter_msg}" == "${current_file_linter_msg}" ]]; then + still_exists=true + break + fi + done < "${current_file}" + fi + + # If issue doesn't exist in current, it was fixed + if ! $still_exists; then + echo "${baseline_line}" >> "${fixed_issues_file}" + fi + done < "${baseline_file}" + fi +} + +# Output issue (basic format) +output_issue() { + echo "$1" +} + +# Generate basic report +generate_report() { + local new_issues_file="$1" + local preexisting_issues_file="$2" + local fixed_issues_file="$3" + local baseline_file="$4" + + local new_count=0 + local preexisting_count=0 + local fixed_count=0 + local baseline_count=0 + + [[ -f "${new_issues_file}" ]] && new_count=$(wc -l < "${new_issues_file}" | tr -d ' ') + [[ -f "${preexisting_issues_file}" ]] && preexisting_count=$(wc -l < "${preexisting_issues_file}" | tr -d ' ') + [[ -f "${fixed_issues_file}" ]] && fixed_count=$(wc -l < "${fixed_issues_file}" | tr -d ' ') + [[ -f "${baseline_file}" ]] && baseline_count=$(wc -l < "${baseline_file}" | tr -d ' ') + + local current_total=$((new_count + preexisting_count)) + + # Summary header + echo "API Lint Diff Results" + echo "=====================" + echo "Baseline (${BASELINE_BRANCH}): ${baseline_count} issues" + echo "Current branch: ${current_total} issues" + echo "" + echo "FIXED: ${fixed_count}" + echo "NEW: ${new_count}" + echo "PRE-EXISTING: ${preexisting_count}" + echo "" + + # Show FIXED issues + if [[ ${fixed_count} -gt 0 ]]; then + echo "=== FIXED ISSUES ===" + while IFS= read -r line; do + output_issue "${line}" + done < "${fixed_issues_file}" + echo "" + fi + + # Show NEW issues + if [[ ${new_count} -gt 0 ]]; then + echo "=== NEW ISSUES ===" + while IFS= read -r line; do + output_issue "${line}" + done < "${new_issues_file}" + echo "" + fi + + # Show PRE-EXISTING issues + if [[ ${preexisting_count} -gt 0 ]]; then + echo "=== PRE-EXISTING ISSUES ===" + while IFS= read -r line; do + output_issue "${line}" + done < "${preexisting_issues_file}" + echo "" + fi + + # Exit based on NEW issues count + if [[ ${new_count} -eq 0 ]]; then + if [[ ${fixed_count} -gt 0 ]]; then + echo -e "${GREEN}SUCCESS: Fixed ${fixed_count} issue(s), no new issues introduced.${NC}" + else + echo -e "${GREEN}NO NEW ISSUES found. Lint check passed.${NC}" + fi + if [[ ${preexisting_count} -gt 0 ]]; then + echo -e "${YELLOW}WARNING: ${preexisting_count} pre-existing issue(s) remain. Please address them separately.${NC}" + fi + return 0 + else + echo -e "${RED}FAILED: ${new_count} new issue(s) introduced${NC}" + return 1 + fi +} + +# Main execution +main() { + # Find golangci-lint + BASE_LINTER_PATH=$(find_golangci_lint) + + # Build custom linter with kube-api-linter plugin + LINTER_PATH="${BASE_LINTER_PATH}" + if CUSTOM_LINTER=$(build_custom_linter "${BASE_LINTER_PATH}"); then + LINTER_PATH="${CUSTOM_LINTER}" + fi + + # Convert to absolute path if needed + if [[ "${LINTER_PATH}" != /* ]]; then + LINTER_PATH="$(pwd)/${LINTER_PATH}" + fi + + # Create temporary config + create_temp_config + + # Ensure baseline branch is available (important for CI environments like GitHub Actions) + if ! git rev-parse --verify "${BASELINE_BRANCH}" &> /dev/null; then + echo -e "${YELLOW}Baseline branch '${BASELINE_BRANCH}' not found locally. Fetching from origin...${NC}" >&2 + + # Fetch the baseline branch from origin + if ! git fetch origin "${BASELINE_BRANCH}:${BASELINE_BRANCH}" 2>&1; then + # If direct fetch fails, try fetching with remote tracking + if ! git fetch origin "${BASELINE_BRANCH}" 2>&1; then + echo -e "${RED}Error: Failed to fetch baseline branch '${BASELINE_BRANCH}' from origin${NC}" >&2 + echo -e "${RED}Please ensure the branch exists in the remote repository.${NC}" >&2 + exit 1 + fi + # Use the remote tracking branch, adding 'origin/' only if not already present + if [[ "${BASELINE_BRANCH}" != origin/* ]]; then + BASELINE_BRANCH="origin/${BASELINE_BRANCH}" + fi + fi + fi + + # Get changed files + get_changed_files > "${TEMP_DIR}/changed_files.txt" + + # Run linter on current branch + REPO_ROOT="$(pwd)" + run_linter "${TEMP_DIR}/.golangci.yaml" "${TEMP_DIR}/current_output.txt" "${LINTER_PATH}" "${REPO_ROOT}" + parse_linter_output "${TEMP_DIR}/current_output.txt" "${TEMP_DIR}/current_parsed.txt" + + # Run linter on baseline + WORKTREE_DIR="${TEMP_DIR}/baseline_worktree" + if ! git worktree add --detach "${WORKTREE_DIR}" "${BASELINE_BRANCH}" 2>&1; then + echo -e "${RED}Error: Failed to create git worktree for baseline branch '${BASELINE_BRANCH}'${NC}" >&2 + echo -e "${RED}Please ensure the branch exists and try again.${NC}" >&2 + exit 1 + fi + run_linter "${TEMP_DIR}/.golangci.yaml" "${TEMP_DIR}/baseline_output.txt" "${LINTER_PATH}" "${WORKTREE_DIR}" + parse_linter_output "${TEMP_DIR}/baseline_output.txt" "${TEMP_DIR}/baseline_parsed.txt" + # Worktree cleanup is handled by the cleanup trap + + # Categorize issues + touch "${TEMP_DIR}/new_issues.txt" + touch "${TEMP_DIR}/preexisting_issues.txt" + touch "${TEMP_DIR}/fixed_issues.txt" + + categorize_issues \ + "${TEMP_DIR}/current_parsed.txt" \ + "${TEMP_DIR}/baseline_parsed.txt" \ + "${TEMP_DIR}/changed_files.txt" \ + "${TEMP_DIR}/new_issues.txt" \ + "${TEMP_DIR}/preexisting_issues.txt" \ + "${TEMP_DIR}/fixed_issues.txt" + + # Generate report + generate_report \ + "${TEMP_DIR}/new_issues.txt" \ + "${TEMP_DIR}/preexisting_issues.txt" \ + "${TEMP_DIR}/fixed_issues.txt" \ + "${TEMP_DIR}/baseline_parsed.txt" + + return $? +} + +# Run main function +main "$@" diff --git a/hack/ci/custom-linters/analyzers/testdata/go.mod b/hack/ci/custom-linters/analyzers/testdata/go.mod index 23875e2335..6a5571ff39 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.24.3 +go 1.25.3 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 7ef38a47f2..b9b2a8ff03 100644 --- a/hack/ci/custom-linters/analyzers/testdata/go.sum +++ b/hack/ci/custom-linters/analyzers/testdata/go.sum @@ -1,4 +1,2 @@ -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/conftest/policy/README.md b/hack/conftest/policy/README.md new file mode 100644 index 0000000000..ff1a16bc67 --- /dev/null +++ b/hack/conftest/policy/README.md @@ -0,0 +1,70 @@ +# OPA Policies for NetworkPolicy Validation + +This directory contains [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) Rego policies used by [conftest](https://www.conftest.dev/) to validate generated Kubernetes manifests. + +## Policy Files + +### olm-networkpolicies.rego + +Package: `main` + +Validates core OLM NetworkPolicy requirements: + +- **Deny-all policy**: Ensures a default deny-all NetworkPolicy exists with empty podSelector and both Ingress/Egress policy types +- **catalogd-controller-manager policy**: Validates the NetworkPolicy for catalogd: + - Ingress on port 7443 (Prometheus metrics scraping) + - Ingress on port 8443 (catalog metadata queries from operator-controller) + - Ingress on port 9443 (Kubernetes API server webhook access) + - General egress enabled +- **operator-controller-controller-manager policy**: Validates the NetworkPolicy for operator-controller: + - Ingress on port 8443 (Prometheus metrics scraping) + - General egress enabled (for pulling bundle images, connecting to catalogd, and Kubernetes API) + +### prometheus-networkpolicies.rego + +Package: `prometheus` + +Validates Prometheus NetworkPolicy requirements: + +- Ensures a NetworkPolicy exists that allows both ingress and egress traffic for prometheus pods + +## Usage + +These policies are automatically run as part of: + +- `make lint-helm` - Validates both helm/olmv1 and helm/prometheus charts (runs `main` and `prometheus` packages) +- `make manifests` - Generates and validates core OLM manifests using only `main` package policies + (Prometheus policies are intentionally skipped here, even if manifests include Prometheus resources; + they are validated via `make lint-helm`) + +### Running manually + +```bash +# Run all policies (main + prometheus namespaces) +(helm template olmv1 helm/olmv1; helm template prometheus helm/prometheus) | conftest test --policy hack/conftest/policy/ --combine -n main -n prometheus - + +# Run only OLM policies +helm template olmv1 helm/olmv1 | conftest test --policy hack/conftest/policy/ --combine -n main - + +# Run only prometheus policies +helm template prometheus helm/prometheus | conftest test --policy hack/conftest/policy/ --combine -n prometheus - +``` + +### Excluding policies + +Use the `-n` (namespace) flag to selectively run policies: + +```bash +# Skip prometheus policies +conftest test --policy hack/conftest/policy/ --combine -n main + +# Skip OLM policies +conftest test --policy hack/conftest/policy/ --combine -n prometheus +``` + +## Adding New Policies + +1. Add new rules to an existing `.rego` file or create a new one +2. Use `package main` for policies that should run by default on all manifests +3. Use a custom package name (e.g., `package prometheus`) for optional policies +4. Update the Makefile targets if new namespaces need to be included diff --git a/hack/conftest/policy/olm-networkpolicies.rego b/hack/conftest/policy/olm-networkpolicies.rego new file mode 100644 index 0000000000..493a4fdd60 --- /dev/null +++ b/hack/conftest/policy/olm-networkpolicies.rego @@ -0,0 +1,160 @@ +package main + +import rego.v1 + +# Check that a deny-all NetworkPolicy exists +# A deny-all policy has: +# - podSelector: {} (empty, applies to all pods) +# - policyTypes containing both "Ingress" and "Egress" +# - No ingress or egress rules defined + +is_deny_all(policy) if { + policy.kind == "NetworkPolicy" + policy.apiVersion == "networking.k8s.io/v1" + + # podSelector must be empty (applies to all pods) + count(policy.spec.podSelector) == 0 + + # Must have both Ingress and Egress policy types + policy_types := {t | some t in policy.spec.policyTypes} + policy_types["Ingress"] + policy_types["Egress"] + + # Must not have any ingress rules + not policy.spec.ingress + + # Must not have any egress rules + not policy.spec.egress +} + +has_deny_all_policy if { + some i in numbers.range(0, count(input) - 1) + is_deny_all(input[i].contents) +} + +deny contains msg if { + not has_deny_all_policy + msg := "No deny-all NetworkPolicy found. A NetworkPolicy with empty podSelector, policyTypes [Ingress, Egress], and no ingress/egress rules is required." +} + +# Check that a NetworkPolicy exists for catalogd-controller-manager that: +# - Allows ingress on TCP ports 7443, 8443, 9443 +# - Allows general egress traffic + +is_catalogd_policy(policy) if { + policy.kind == "NetworkPolicy" + policy.apiVersion == "networking.k8s.io/v1" + policy.spec.podSelector.matchLabels["control-plane"] == "catalogd-controller-manager" +} + +catalogd_policies contains policy if { + some i in numbers.range(0, count(input) - 1) + policy := input[i].contents + is_catalogd_policy(policy) +} + +catalogd_ingress_ports contains port if { + some policy in catalogd_policies + some rule in policy.spec.ingress + some port in rule.ports + port.protocol == "TCP" +} + +catalogd_ingress_port_numbers contains num if { + some port in catalogd_ingress_ports + num := port.port +} + +catalogd_has_egress if { + some policy in catalogd_policies + policy.spec.egress +} + +deny contains msg if { + count(catalogd_policies) == 0 + msg := "No NetworkPolicy found for catalogd-controller-manager. A NetworkPolicy allowing ingress on TCP ports 7443, 8443, 9443 and general egress is required." +} + +deny contains msg if { + count(catalogd_policies) > 1 + msg := sprintf("Expected exactly 1 NetworkPolicy for catalogd-controller-manager, found %d.", [count(catalogd_policies)]) +} + +deny contains msg if { + count(catalogd_policies) == 1 + not catalogd_ingress_port_numbers[7443] + msg := "Allow traffic to port 7443. Permit Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health." +} + +deny contains msg if { + count(catalogd_policies) == 1 + not catalogd_ingress_port_numbers[8443] + msg := "Allow traffic to port 8443. Permit clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery." +} + +deny contains msg if { + count(catalogd_policies) == 1 + not catalogd_ingress_port_numbers[9443] + msg := "Allow traffic to port 9443. Permit Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources." +} + +deny contains msg if { + count(catalogd_policies) == 1 + not catalogd_has_egress + msg := "Missing egress rules in catalogd-controller-manager NetworkPolicy. General egress is required to enable catalogd-controller to pull bundle images from arbitrary image registries, and interact with the Kubernetes API server." +} + +# Check that a NetworkPolicy exists for operator-controller-controller-manager that: +# - Allows ingress on TCP port 8443 +# - Allows general egress traffic + +is_operator_controller_policy(policy) if { + policy.kind == "NetworkPolicy" + policy.apiVersion == "networking.k8s.io/v1" + policy.spec.podSelector.matchLabels["control-plane"] == "operator-controller-controller-manager" +} + +operator_controller_policies contains policy if { + some i in numbers.range(0, count(input) - 1) + policy := input[i].contents + is_operator_controller_policy(policy) +} + +operator_controller_ingress_ports contains port if { + some policy in operator_controller_policies + some rule in policy.spec.ingress + some port in rule.ports + port.protocol == "TCP" +} + +operator_controller_ingress_port_numbers contains num if { + some port in operator_controller_ingress_ports + num := port.port +} + +operator_controller_has_egress if { + some policy in operator_controller_policies + policy.spec.egress +} + +deny contains msg if { + count(operator_controller_policies) == 0 + msg := "No NetworkPolicy found for operator-controller-controller-manager. A NetworkPolicy allowing ingress on TCP port 8443 and general egress is required." +} + +deny contains msg if { + count(operator_controller_policies) > 1 + msg := sprintf("Expected exactly 1 NetworkPolicy for operator-controller-controller-manager, found %d.", [count(operator_controller_policies)]) +} + +deny contains msg if { + count(operator_controller_policies) == 1 + not operator_controller_ingress_port_numbers[8443] + msg := "Allow traffic to port 8443. Permit Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health." +} + +deny contains msg if { + count(operator_controller_policies) == 1 + not operator_controller_has_egress + msg := "Missing egress rules in operator-controller-controller-manager NetworkPolicy. General egress is required to enable 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." +} diff --git a/hack/conftest/policy/prometheus-networkpolicies.rego b/hack/conftest/policy/prometheus-networkpolicies.rego new file mode 100644 index 0000000000..c37158250c --- /dev/null +++ b/hack/conftest/policy/prometheus-networkpolicies.rego @@ -0,0 +1,33 @@ +package prometheus + +import rego.v1 + +# Check that a NetworkPolicy exists that allows both ingress and egress traffic to prometheus pods +is_prometheus_policy(policy) if { + policy.kind == "NetworkPolicy" + policy.apiVersion == "networking.k8s.io/v1" + + # Must target prometheus pods + policy.spec.podSelector.matchLabels["app.kubernetes.io/name"] == "prometheus" + + # Must have both Ingress and Egress policy types + policy_types := {t | some t in policy.spec.policyTypes} + policy_types["Ingress"] + policy_types["Egress"] + + # Must have ingress rules defined (allowing traffic) + policy.spec.ingress + + # Must have egress rules defined (allowing traffic) + policy.spec.egress +} + +has_prometheus_policy if { + some i in numbers.range(0, count(input) - 1) + is_prometheus_policy(input[i].contents) +} + +deny contains msg if { + not has_prometheus_policy + msg := "No NetworkPolicy found that allows both ingress and egress traffic to prometheus pods. A NetworkPolicy targeting prometheus pods with ingress and egress rules is required." +} diff --git a/hack/demo/own-namespace-demo-script.sh b/hack/demo/own-namespace-demo-script.sh index 611c6dfb05..86b3d28760 100755 --- a/hack/demo/own-namespace-demo-script.sh +++ b/hack/demo/own-namespace-demo-script.sh @@ -6,16 +6,14 @@ set -e trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# install experimental CRDs with config field support -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/experimental.yaml" +# install standard CRDs +echo "Install standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" -# wait for experimental CRDs to be available +# wait for standard CRDs to be available kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io -# 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"}]' - -# wait for operator-controller to become available +# Ensure controller is healthy kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -57,17 +55,6 @@ 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 -# remove feature gate from deployment -echo "Removing feature gate from operator-controller..." -kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/args", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' || true - -# restore standard CRDs -echo "Restoring standard CRDs..." -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/base.yaml" - -# wait for standard CRDs to be available -kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io - # wait for operator-controller to become available with standard config kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager diff --git a/hack/demo/single-namespace-demo-script.sh b/hack/demo/single-namespace-demo-script.sh index 9702684152..885854dd9d 100755 --- a/hack/demo/single-namespace-demo-script.sh +++ b/hack/demo/single-namespace-demo-script.sh @@ -6,16 +6,14 @@ set -e trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# install experimental CRDs with config field support -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/experimental.yaml" +# install standard CRDs +echo "Install standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" -# wait for experimental CRDs to be available +# wait for standard CRDs to be available kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io -# 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"}]' - -# wait for operator-controller to become available +# Ensure controller is healthy kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -60,17 +58,6 @@ 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 -# remove feature gate from deployment -echo "Removing feature gate from operator-controller..." -kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/args", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' || true - -# restore standard CRDs -echo "Restoring standard CRDs..." -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/base.yaml" - -# wait for standard CRDs to be available -kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io - # wait for operator-controller to become available with standard config kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager diff --git a/hack/test/pre-upgrade-setup.sh b/hack/test/pre-upgrade-setup.sh index 669f9da37f..f2dc28761c 100755 --- a/hack/test/pre-upgrade-setup.sh +++ b/hack/test/pre-upgrade-setup.sh @@ -122,6 +122,19 @@ rules: - "update" resourceNames: - "${TEST_CLUSTER_EXTENSION_NAME}" + - apiGroups: + - "olm.operatorframework.io" + resources: + - "clusterextensionrevisions" + - "clusterextensionrevisions/finalizers" + verbs: + - "create" + - "update" + - "patch" + - "delete" + - "get" + - "list" + - "watch" EOF kubectl apply -f - < // // +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 + // +required Namespace string `json:"namespace"` // serviceAccount is a reference to a ServiceAccount used to perform all interactions @@ -69,7 +69,7 @@ type ClusterExtensionSpec struct { // The ServiceAccount must exist in the namespace referenced in the spec. // serviceAccount is required. // - // +kubebuilder:validation:Required + // +required ServiceAccount ServiceAccountReference `json:"serviceAccount"` // source is a required field which selects the installation source of content @@ -85,7 +85,7 @@ type ClusterExtensionSpec struct { // catalog: // packageName: example-package // - // +kubebuilder:validation:Required + // +required Source SourceConfig `json:"source"` // install is an optional field used to configure the installation options @@ -114,7 +114,7 @@ type SourceConfig struct { // +unionDiscriminator // // - // +kubebuilder:validation:Required + // +required SourceType string `json:"sourceType"` // catalog is used to configure how information is sourced from a catalog. @@ -180,7 +180,7 @@ type CatalogFilter struct { // +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 + // +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. @@ -370,7 +370,7 @@ type ServiceAccountReference struct { // +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 + // +required Name string `json:"name"` } @@ -400,7 +400,7 @@ type CRDUpgradeSafetyPreflightConfig struct { // performing an upgrade operation. // // +kubebuilder:validation:Enum:="None";"Strict" - // +kubebuilder:validation:Required + // +required Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"` } @@ -425,14 +425,14 @@ type BundleMetadata struct { // hyphens (-) or periods (.), start and end with an alphanumeric character, // and be no longer than 253 characters. // - // +kubebuilder:validation:Required + // +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 + // +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"` } @@ -475,7 +475,7 @@ type ClusterExtensionInstallStatus struct { // 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 + // +required Bundle BundleMetadata `json:"bundle"` } @@ -513,7 +513,7 @@ type ClusterExtensionList struct { // items is a required list of ClusterExtension objects. // - // +kubebuilder:validation:Required + // +required Items []ClusterExtension `json:"items"` } 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 index 9866f68bbe..3ee03a42fd 100644 --- 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensions.olm.operatorframework.io spec: diff --git a/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml b/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml index 8dcb4beed1..161f1b3836 100644 --- a/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml +++ b/hack/tools/crd-generator/testdata/output/standard/olm.operatorframework.io_clusterextensions.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clusterextensions.olm.operatorframework.io spec: diff --git a/hack/tools/schema-generator/main.go b/hack/tools/schema-generator/main.go new file mode 100644 index 0000000000..aedea32ebb --- /dev/null +++ b/hack/tools/schema-generator/main.go @@ -0,0 +1,436 @@ +package main + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + schemaID = "https://operator-framework.io/schemas/registry-v1-bundle-config.json" + schemaDraft = "http://json-schema.org/draft-07/schema#" + schemaTitle = "Registry+v1 Bundle Configuration" + schemaDescription = "Configuration schema for registry+v1 bundles. Includes watchNamespace for controlling operator scope and deploymentConfig for customizing operator deployment (environment variables, resource scheduling, storage, and pod placement). The deploymentConfig follows the same structure and behavior as OLM v0's SubscriptionConfig. Note: The 'selector' field from v0's SubscriptionConfig is not included as it was never used." +) + +// OpenAPISpec represents the structure of Kubernetes OpenAPI v3 spec +type OpenAPISpec struct { + Components struct { + Schemas map[string]interface{} `json:"schemas"` + } `json:"components"` +} + +// Schema represents a JSON Schema Draft 7 document with OpenAPI v3 components +type Schema struct { + Schema string `json:"$schema"` + ID string `json:"$id"` + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` + Properties map[string]*SchemaField `json:"properties"` + AdditionalProperties bool `json:"additionalProperties"` + Components map[string]interface{} `json:"components,omitempty"` +} + +// SchemaField represents a single field in a JSON Schema +type SchemaField struct { + Type string `json:"type,omitempty"` + Description string `json:"description,omitempty"` + Properties map[string]*SchemaField `json:"properties,omitempty"` + AdditionalProperties interface{} `json:"additionalProperties,omitempty"` + Items interface{} `json:"items,omitempty"` + AnyOf []*SchemaField `json:"anyOf,omitempty"` + AllOf []*SchemaField `json:"allOf,omitempty"` + Ref string `json:"$ref,omitempty"` + + // Allow pass-through of unknown fields from OpenAPI schemas + Extra map[string]interface{} `json:"-"` +} + +// FieldInfo contains parsed information about a struct field +type FieldInfo struct { + JSONName string + TypeName string + TypePkg string + IsSlice bool + IsPtr bool + IsMap bool +} + +// schemaCollector tracks schemas that need to be included for $ref resolution +type schemaCollector struct { + openAPISpec *OpenAPISpec + collectedSchemas map[string]bool +} + +func main() { + if len(os.Args) != 4 { + fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + os.Exit(1) + } + + k8sOpenAPISpecURL := os.Args[1] + subscriptionTypesFile := os.Args[2] + outputFile := os.Args[3] + + fmt.Printf("Fetching Kubernetes OpenAPI spec from %s...\n", k8sOpenAPISpecURL) + + // Fetch the Kubernetes OpenAPI spec + openAPISpec, err := fetchOpenAPISpec(k8sOpenAPISpecURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching OpenAPI spec: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Parsing SubscriptionConfig from %s...\n", subscriptionTypesFile) + + // Parse SubscriptionConfig structure + fields, err := parseSubscriptionConfig(subscriptionTypesFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing SubscriptionConfig: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Generating registry+v1 bundle configuration schema...\n") + + // Generate the schema + schema := generateBundleConfigSchema(openAPISpec, fields) + + // Marshal to JSON with indentation + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling schema: %v\n", err) + os.Exit(1) + } + + // Ensure output directory exists + dir := filepath.Dir(outputFile) + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err) + os.Exit(1) + } + + // Write to file + if err := os.WriteFile(outputFile, data, 0600); err != nil { + fmt.Fprintf(os.Stderr, "Error writing schema file: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully generated schema at %s\n", outputFile) +} + +func fetchOpenAPISpec(url string) (*OpenAPISpec, error) { + // Create HTTP client with timeout to prevent hanging + client := &http.Client{ + Timeout: 30 * time.Second, + } + + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to fetch spec: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + var spec OpenAPISpec + if err := json.Unmarshal(body, &spec); err != nil { + return nil, fmt.Errorf("failed to unmarshal spec: %w", err) + } + + return &spec, nil +} + +func parseSubscriptionConfig(filePath string) ([]FieldInfo, error) { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + var fields []FieldInfo + + // Find the SubscriptionConfig struct + ast.Inspect(node, func(n ast.Node) bool { + typeSpec, ok := n.(*ast.TypeSpec) + if !ok || typeSpec.Name.Name != "SubscriptionConfig" { + return true + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + return true + } + + // Extract field information + for _, field := range structType.Fields.List { + if field.Names == nil { + continue + } + + fieldName := field.Names[0].Name + + // Skip Selector field + if fieldName == "Selector" { + continue + } + + // Get JSON tag + jsonName := extractJSONTag(field.Tag) + if jsonName == "" || jsonName == "-" { + continue + } + + // Parse the field type + fieldInfo := FieldInfo{ + JSONName: jsonName, + } + + parseFieldType(field.Type, &fieldInfo) + + fields = append(fields, fieldInfo) + } + + return false + }) + + return fields, nil +} + +func extractJSONTag(tag *ast.BasicLit) string { + if tag == nil { + return "" + } + + tagValue := strings.Trim(tag.Value, "`") + for _, part := range strings.Split(tagValue, " ") { + if strings.HasPrefix(part, "json:") { + jsonTag := strings.Trim(strings.TrimPrefix(part, "json:"), "\"") + return strings.Split(jsonTag, ",")[0] + } + } + + return "" +} + +func parseFieldType(expr ast.Expr, info *FieldInfo) { + switch t := expr.(type) { + case *ast.ArrayType: + info.IsSlice = true + parseFieldType(t.Elt, info) + + case *ast.StarExpr: + info.IsPtr = true + parseFieldType(t.X, info) + + case *ast.MapType: + info.IsMap = true + info.TypeName = "map[string]string" // Simplified for our use case + + case *ast.Ident: + info.TypeName = t.Name + + case *ast.SelectorExpr: + if pkg, ok := t.X.(*ast.Ident); ok { + info.TypePkg = pkg.Name + info.TypeName = t.Sel.Name + } + } +} + +func generateBundleConfigSchema(openAPISpec *OpenAPISpec, fields []FieldInfo) *Schema { + schema := &Schema{ + Schema: schemaDraft, + ID: schemaID, + Title: schemaTitle, + Description: schemaDescription, + Type: "object", + Properties: make(map[string]*SchemaField), + AdditionalProperties: false, + } + + // Track schemas we need to include (for resolving $ref dependencies) + collector := &schemaCollector{ + openAPISpec: openAPISpec, + collectedSchemas: make(map[string]bool), + } + + // Add watchNamespace property (base definition - will be modified at runtime) + schema.Properties["watchNamespace"] = &SchemaField{ + Description: "The namespace that the operator should watch for custom resources. The meaning and validation of this field depends on the operator's install modes. This field may be optional or required, and may have format constraints, based on the operator's supported install modes.", + AnyOf: []*SchemaField{ + {Type: "null"}, + {Type: "string"}, + }, + } + + // Create deploymentConfig property + deploymentConfigProps := make(map[string]*SchemaField) + + // Build deploymentConfig properties from parsed fields + for _, field := range fields { + fieldSchema := mapFieldToOpenAPISchema(field, openAPISpec, collector) + if fieldSchema != nil { + deploymentConfigProps[field.JSONName] = fieldSchema + } + } + + schema.Properties["deploymentConfig"] = &SchemaField{ + Type: "object", + Description: "Configuration for customizing operator deployment (environment variables, resources, volumes, etc.)", + Properties: deploymentConfigProps, + AdditionalProperties: false, + } + + // Add all collected schemas to the components/schemas section + // (OpenAPI v3 uses components/schemas for $ref resolution) + if len(collector.collectedSchemas) > 0 { + componentsSchemas := make(map[string]interface{}) + for schemaName := range collector.collectedSchemas { + if s, ok := openAPISpec.Components.Schemas[schemaName]; ok { + componentsSchemas[schemaName] = s + } + } + + schema.Components = map[string]interface{}{ + "schemas": componentsSchemas, + } + } + + return schema +} + +func mapFieldToOpenAPISchema(field FieldInfo, openAPISpec *OpenAPISpec, collector *schemaCollector) *SchemaField { + // Handle map types (nodeSelector, annotations) + if field.IsMap { + return &SchemaField{ + Type: "object", + AdditionalProperties: &SchemaField{ + Type: "string", + }, + } + } + + // Get the OpenAPI schema for the base type + openAPITypeName := getOpenAPITypeName(field) + if openAPITypeName == "" { + fmt.Fprintf(os.Stderr, "Warning: Could not map field %s (type: %s.%s) to OpenAPI schema\n", + field.JSONName, field.TypePkg, field.TypeName) + return nil + } + + baseSchema, ok := openAPISpec.Components.Schemas[openAPITypeName] + if !ok { + fmt.Fprintf(os.Stderr, "Warning: Schema for %s not found in OpenAPI spec\n", openAPITypeName) + return nil + } + + // Collect this schema and all its dependencies + collector.collectSchemaWithDependencies(openAPITypeName, baseSchema) + + // Use $ref to point to the schema in components/schemas. + // This preserves all validation keywords (required, enum, format, pattern, etc.) + // that would be lost if we copied the schema content via marshal/unmarshal. + schemaRef := &SchemaField{ + Ref: fmt.Sprintf("#/components/schemas/%s", openAPITypeName), + } + + // Wrap in array if it's a slice field + if field.IsSlice { + return &SchemaField{ + Type: "array", + Items: schemaRef, + } + } + + return schemaRef +} + +// collectSchemaWithDependencies recursively collects a schema and all schemas it references via $ref +func (c *schemaCollector) collectSchemaWithDependencies(schemaName string, schema interface{}) { + // Mark this schema as collected + if c.collectedSchemas[schemaName] { + return // Already processed + } + c.collectedSchemas[schemaName] = true + + // Recursively find all $ref references in this schema + c.findReferences(schema) +} + +// findReferences recursively walks a schema object to find all $ref pointers +func (c *schemaCollector) findReferences(obj interface{}) { + switch v := obj.(type) { + case map[string]interface{}: + // Check if this is a $ref and process it + c.processRef(v) + + // Recursively check all values in the map + for _, val := range v { + c.findReferences(val) + } + + case []interface{}: + // Recursively check all items in the array + for _, item := range v { + c.findReferences(item) + } + } +} + +// processRef extracts and collects schema dependencies from a $ref pointer +func (c *schemaCollector) processRef(v map[string]interface{}) { + ref, ok := v["$ref"].(string) + if !ok { + return + } + + // Extract the schema name from the $ref + // Format: "#/components/schemas/io.k8s.api.core.v1.NodeAffinity" + if !strings.HasPrefix(ref, "#/components/schemas/") { + return + } + + schemaName := strings.TrimPrefix(ref, "#/components/schemas/") + + // Skip if already collected + if c.collectedSchemas[schemaName] { + return + } + + // Collect the referenced schema recursively + refSchema, ok := c.openAPISpec.Components.Schemas[schemaName] + if ok { + c.collectSchemaWithDependencies(schemaName, refSchema) + } +} + +func getOpenAPITypeName(field FieldInfo) string { + // Map package names to OpenAPI prefixes + pkgMap := map[string]string{ + "corev1": "io.k8s.api.core.v1", + "v1": "io.k8s.api.core.v1", + } + + prefix, ok := pkgMap[field.TypePkg] + if !ok { + return "" + } + + return fmt.Sprintf("%s.%s", prefix, field.TypeName) +} diff --git a/hack/tools/schema-generator/main_test.go b/hack/tools/schema-generator/main_test.go new file mode 100644 index 0000000000..41807d3a02 --- /dev/null +++ b/hack/tools/schema-generator/main_test.go @@ -0,0 +1,333 @@ +package main + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getPackageDir returns the directory path of the specified Go package. +// It uses 'go list' which automatically handles both vendor mode and module cache. +func getPackageDir(t *testing.T, pkgPath string) string { + t.Helper() + cmd := exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath) + out, err := cmd.Output() + require.NoError(t, err, "failed to find package %s", pkgPath) + return strings.TrimSpace(string(out)) +} + +// Mock OpenAPI spec for testing +func getMockOpenAPISpec() *OpenAPISpec { + return &OpenAPISpec{ + Components: struct { + Schemas map[string]interface{} `json:"schemas"` + }{ + Schemas: map[string]interface{}{ + "io.k8s.api.core.v1.Toleration": map[string]interface{}{ + "type": "object", + "description": "The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator .", + "properties": map[string]interface{}{ + "key": map[string]string{"type": "string"}, + "operator": map[string]string{"type": "string"}, + "value": map[string]string{"type": "string"}, + "effect": map[string]string{"type": "string"}, + "tolerationSeconds": map[string]interface{}{"type": "integer", "format": "int64"}, + }, + }, + "io.k8s.api.core.v1.ResourceRequirements": map[string]interface{}{ + "type": "object", + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": map[string]interface{}{ + "limits": map[string]interface{}{"type": "object"}, + "requests": map[string]interface{}{"type": "object"}, + }, + }, + "io.k8s.api.core.v1.EnvVar": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{"name": map[string]string{"type": "string"}}, + }, + "io.k8s.api.core.v1.EnvFromSource": map[string]interface{}{ + "type": "object", + }, + "io.k8s.api.core.v1.Volume": map[string]interface{}{ + "type": "object", + }, + "io.k8s.api.core.v1.VolumeMount": map[string]interface{}{ + "type": "object", + }, + "io.k8s.api.core.v1.Affinity": map[string]interface{}{ + "type": "object", + }, + }, + }, + } +} + +func TestParseSubscriptionConfig(t *testing.T) { + // Get the package directory containing subscription_types.go + pkgDir := getPackageDir(t, "github.com/operator-framework/api/pkg/operators/v1alpha1") + subscriptionTypesFile := filepath.Join(pkgDir, "subscription_types.go") + + fields, err := parseSubscriptionConfig(subscriptionTypesFile) + require.NoError(t, err, "should successfully parse SubscriptionConfig") + require.NotEmpty(t, fields, "should find fields in SubscriptionConfig") + + // Create a map for easier checking + fieldMap := make(map[string]FieldInfo) + for _, field := range fields { + fieldMap[field.JSONName] = field + } + + t.Run("includes expected fields", func(t *testing.T) { + expectedFields := []string{ + "nodeSelector", + "tolerations", + "resources", + "env", + "envFrom", + "volumes", + "volumeMounts", + "affinity", + "annotations", + } + + for _, fieldName := range expectedFields { + assert.Contains(t, fieldMap, fieldName, "should include %s field", fieldName) + } + }) + + t.Run("excludes selector field", func(t *testing.T) { + assert.NotContains(t, fieldMap, "selector", "should exclude selector field per RFC requirement") + }) + + t.Run("parses field types correctly", func(t *testing.T) { + // Check tolerations is a slice + tolerations, ok := fieldMap["tolerations"] + require.True(t, ok, "tolerations should be present") + assert.True(t, tolerations.IsSlice, "tolerations should be a slice") + assert.Equal(t, "corev1", tolerations.TypePkg, "tolerations should be from corev1 package") + assert.Equal(t, "Toleration", tolerations.TypeName, "tolerations type should be Toleration") + + // Check nodeSelector is a map + nodeSelector, ok := fieldMap["nodeSelector"] + require.True(t, ok, "nodeSelector should be present") + assert.True(t, nodeSelector.IsMap, "nodeSelector should be a map") + + // Check resources is an object (pointer) + resources, ok := fieldMap["resources"] + require.True(t, ok, "resources should be present") + assert.Equal(t, "corev1", resources.TypePkg) + assert.Equal(t, "ResourceRequirements", resources.TypeName) + }) +} + +func TestGenerateBundleConfigSchema(t *testing.T) { + mockOpenAPI := getMockOpenAPISpec() + + // Create mock fields similar to what parseSubscriptionConfig would return + fields := []FieldInfo{ + {JSONName: "nodeSelector", IsMap: true}, + {JSONName: "tolerations", TypePkg: "corev1", TypeName: "Toleration", IsSlice: true}, + {JSONName: "resources", TypePkg: "corev1", TypeName: "ResourceRequirements"}, + {JSONName: "annotations", IsMap: true}, + } + + schema := generateBundleConfigSchema(mockOpenAPI, fields) + + t.Run("schema has correct metadata", func(t *testing.T) { + assert.Equal(t, "http://json-schema.org/draft-07/schema#", schema.Schema) + assert.Equal(t, schemaID, schema.ID) + assert.Equal(t, schemaTitle, schema.Title) + assert.NotEmpty(t, schema.Description) + assert.Equal(t, "object", schema.Type) + assert.False(t, schema.AdditionalProperties) + }) + + t.Run("includes watchNamespace property", func(t *testing.T) { + require.Contains(t, schema.Properties, "watchNamespace") + + watchNS := schema.Properties["watchNamespace"] + require.NotNil(t, watchNS) + + assert.NotEmpty(t, watchNS.Description) + assert.Len(t, watchNS.AnyOf, 2, "watchNamespace should have anyOf with null and string") + }) + + t.Run("includes deploymentConfig property", func(t *testing.T) { + require.Contains(t, schema.Properties, "deploymentConfig") + + deployConfig := schema.Properties["deploymentConfig"] + require.NotNil(t, deployConfig) + + assert.Equal(t, "object", deployConfig.Type) + assert.NotEmpty(t, deployConfig.Description) + assert.Equal(t, false, deployConfig.AdditionalProperties) + + // Check that our mock fields are present + assert.Contains(t, deployConfig.Properties, "nodeSelector") + assert.Contains(t, deployConfig.Properties, "tolerations") + assert.Contains(t, deployConfig.Properties, "resources") + assert.Contains(t, deployConfig.Properties, "annotations") + }) +} + +func TestMapFieldToOpenAPISchema(t *testing.T) { + mockOpenAPI := getMockOpenAPISpec() + collector := &schemaCollector{ + openAPISpec: mockOpenAPI, + collectedSchemas: make(map[string]bool), + } + + t.Run("maps map fields correctly", func(t *testing.T) { + field := FieldInfo{ + JSONName: "nodeSelector", + IsMap: true, + } + + schema := mapFieldToOpenAPISchema(field, mockOpenAPI, collector) + require.NotNil(t, schema) + + assert.Equal(t, "object", schema.Type) + assert.NotNil(t, schema.AdditionalProperties) + }) + + t.Run("maps slice fields correctly", func(t *testing.T) { + field := FieldInfo{ + JSONName: "tolerations", + TypePkg: "corev1", + TypeName: "Toleration", + IsSlice: true, + } + + schema := mapFieldToOpenAPISchema(field, mockOpenAPI, collector) + require.NotNil(t, schema) + + assert.Equal(t, "array", schema.Type) + assert.NotNil(t, schema.Items) + + // Items should be a *SchemaField with $ref + items, ok := schema.Items.(*SchemaField) + require.True(t, ok) + assert.Equal(t, "#/components/schemas/io.k8s.api.core.v1.Toleration", items.Ref) + }) + + t.Run("maps object fields correctly", func(t *testing.T) { + field := FieldInfo{ + JSONName: "resources", + TypePkg: "corev1", + TypeName: "ResourceRequirements", + } + + schema := mapFieldToOpenAPISchema(field, mockOpenAPI, collector) + require.NotNil(t, schema) + + // Should be a $ref to the schema in components/schemas + assert.Equal(t, "#/components/schemas/io.k8s.api.core.v1.ResourceRequirements", schema.Ref) + }) +} + +func TestGetOpenAPITypeName(t *testing.T) { + testCases := []struct { + name string + field FieldInfo + expected string + }{ + { + name: "corev1 package", + field: FieldInfo{TypePkg: "corev1", TypeName: "Toleration"}, + expected: "io.k8s.api.core.v1.Toleration", + }, + { + name: "v1 package", + field: FieldInfo{TypePkg: "v1", TypeName: "ResourceRequirements"}, + expected: "io.k8s.api.core.v1.ResourceRequirements", + }, + { + name: "unknown package", + field: FieldInfo{TypePkg: "unknown", TypeName: "SomeType"}, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := getOpenAPITypeName(tc.field) + assert.Equal(t, tc.expected, result) + }) + } +} + +// TestSchemaIsValidJSON verifies that the generated schema is valid JSON +func TestSchemaIsValidJSON(t *testing.T) { + mockOpenAPI := getMockOpenAPISpec() + fields := []FieldInfo{ + {JSONName: "tolerations", TypePkg: "corev1", TypeName: "Toleration", IsSlice: true}, + } + + schema := generateBundleConfigSchema(mockOpenAPI, fields) + + // Marshal to JSON + data, err := json.MarshalIndent(schema, "", " ") + require.NoError(t, err, "should marshal schema to JSON") + + // Unmarshal back to verify it's valid + var unmarshaled map[string]interface{} + err = json.Unmarshal(data, &unmarshaled) + require.NoError(t, err, "generated JSON should be valid and unmarshalable") + + // Verify key top-level fields exist + assert.Contains(t, unmarshaled, "$schema") + assert.Contains(t, unmarshaled, "$id") + assert.Contains(t, unmarshaled, "type") + assert.Contains(t, unmarshaled, "properties") +} + +// TestGeneratedSchemaMatchesActualOutput validates that the checked-in schema file +// has the expected structure and required fields. +func TestGeneratedSchemaMatchesActualOutput(t *testing.T) { + // Read the checked-in schema file + schemaPath := "../../../internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json" + data, err := os.ReadFile(schemaPath) + require.NoError(t, err, "should be able to read the generated schema file") + + // Unmarshal it + var schemaFromFile map[string]interface{} + err = json.Unmarshal(data, &schemaFromFile) + require.NoError(t, err, "checked-in schema should be valid JSON") + + // Verify it has the expected structure + assert.Equal(t, "http://json-schema.org/draft-07/schema#", schemaFromFile["$schema"]) + assert.Equal(t, schemaID, schemaFromFile["$id"]) + assert.Contains(t, schemaFromFile, "properties") + + props, ok := schemaFromFile["properties"].(map[string]interface{}) + require.True(t, ok) + + assert.Contains(t, props, "watchNamespace") + assert.Contains(t, props, "deploymentConfig") + + // Verify deploymentConfig has expected fields + deployConfig, ok := props["deploymentConfig"].(map[string]interface{}) + require.True(t, ok) + + dcProps, ok := deployConfig["properties"].(map[string]interface{}) + require.True(t, ok) + + expectedFields := []string{ + "nodeSelector", "tolerations", "resources", "env", "envFrom", + "volumes", "volumeMounts", "affinity", "annotations", + } + + for _, field := range expectedFields { + assert.Contains(t, dcProps, field, "deploymentConfig should include %s", field) + } + + // Verify selector is NOT present + assert.NotContains(t, dcProps, "selector", "selector field should be excluded per RFC requirement") +} diff --git a/hack/tools/test-profiling/go.mod b/hack/tools/test-profiling/go.mod index df225c4273..11c55a0d85 100644 --- a/hack/tools/test-profiling/go.mod +++ b/hack/tools/test-profiling/go.mod @@ -1,6 +1,6 @@ module github.com/operator-framework/operator-controller/hack/tools/test-profiling -go 1.24.6 +go 1.25.3 require ( github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d diff --git a/hack/tools/update-registryv1-bundle-schema.sh b/hack/tools/update-registryv1-bundle-schema.sh new file mode 100755 index 0000000000..45c27cdb99 --- /dev/null +++ b/hack/tools/update-registryv1-bundle-schema.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# This script generates the registry+v1 bundle configuration JSON schema +# by extracting field information from v1alpha1.SubscriptionConfig and mapping +# those fields to their corresponding Kubernetes OpenAPI v3 schemas. + +# Get the module path from go mod cache +if ! MODULE_PATH=$(go list -mod=readonly -m -f "{{.Dir}}" github.com/operator-framework/api); then + echo "Error: Could not find github.com/operator-framework/api module" >&2 + exit 1 +fi + +# Source files +SUBSCRIPTION_TYPES="${MODULE_PATH}/pkg/operators/v1alpha1/subscription_types.go" + +# Output file +SCHEMA_OUTPUT="internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json" + +# Verify required source file exists +if [[ ! -f "${SUBSCRIPTION_TYPES}" ]]; then + echo "Error: ${SUBSCRIPTION_TYPES} not found." >&2 + echo "Module path: ${MODULE_PATH}" >&2 + exit 1 +fi + +# Get the effective k8s.io/api version (honors replace directives) +if ! K8S_API_VERSION=$(go list -m -f '{{.Version}}' k8s.io/api); then + echo "Error: Could not determine k8s.io/api version" >&2 + exit 1 +fi +if [[ -z "${K8S_API_VERSION}" ]]; then + echo "Error: k8s.io/api version is empty" >&2 + exit 1 +fi + +# Convert k8s.io/api version (v0.35.0) to Kubernetes version (v1.35.0) +# k8s.io/api uses v0.X.Y while Kubernetes uses v1.X.Y +K8S_VERSION=$(echo "${K8S_API_VERSION}" | sed 's/^v0\./v1./' | tr -d '\n') + +echo "$(date '+%Y/%m/%d %T') Detected k8s.io/api version: ${K8S_API_VERSION}" +echo "$(date '+%Y/%m/%d %T') Using Kubernetes version: ${K8S_VERSION}" + +# Construct OpenAPI spec URL +OPENAPI_SPEC_URL="https://raw.githubusercontent.com/kubernetes/kubernetes/refs/tags/${K8S_VERSION}/api/openapi-spec/v3/api__v1_openapi.json" + +echo "$(date '+%Y/%m/%d %T') Fetching Kubernetes OpenAPI spec from: ${OPENAPI_SPEC_URL}" +echo "$(date '+%Y/%m/%d %T') Generating registry+v1 bundle configuration JSON schema..." + +# Run the schema generator +go run ./hack/tools/schema-generator "${OPENAPI_SPEC_URL}" "${SUBSCRIPTION_TYPES}" "${SCHEMA_OUTPUT}" + +echo "$(date '+%Y/%m/%d %T') Schema generation complete: ${SCHEMA_OUTPUT}" diff --git a/helm/experimental.yaml b/helm/experimental.yaml index b14b1b3034..1d27617149 100644 --- a/helm/experimental.yaml +++ b/helm/experimental.yaml @@ -9,7 +9,6 @@ options: operatorController: features: enabled: - - SingleOwnNamespaceInstallSupport - PreflightPermissions - HelmChartSupport - BoxcutterRuntime diff --git a/helm/olmv1/base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml b/helm/olmv1/base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml index c78a57b925..39d6e906fc 100644 --- a/helm/olmv1/base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml +++ b/helm/olmv1/base/catalogd/crd/experimental/olm.operatorframework.io_clustercatalogs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clustercatalogs.olm.operatorframework.io spec: @@ -29,7 +29,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -51,29 +51,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -81,35 +76,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -120,25 +113,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -221,12 +212,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -244,31 +234,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -329,11 +318,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -342,14 +329,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -383,12 +370,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -407,19 +393,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: diff --git a/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml b/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml index 94f1d7121d..1a97fdfe0a 100644 --- a/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml +++ b/helm/olmv1/base/catalogd/crd/standard/olm.operatorframework.io_clustercatalogs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clustercatalogs.olm.operatorframework.io spec: @@ -29,7 +29,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -51,29 +51,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -81,35 +76,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -120,25 +113,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -221,12 +212,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -244,31 +234,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -329,11 +318,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -342,14 +329,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -383,12 +370,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -407,19 +393,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: 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 index c448a4da46..31e267b308 100644 --- 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensionrevisions.olm.operatorframework.io spec: @@ -166,6 +166,16 @@ spec: x-kubernetes-validations: - message: phases is immutable rule: self == oldSelf || oldSelf.size() == 0 + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer revision: description: |- revision is a required, immutable sequence number representing a specific revision 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 index b51817c166..b857b9c1c5 100644 --- 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensions.olm.operatorframework.io spec: @@ -59,31 +59,29 @@ spec: 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 optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is 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. + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. properties: configType: description: |- - configType is a required reference to the type of configuration source. + configType is required and specifies the type of configuration source. - Allowed values are "Inline" + The only allowed value is "Inline". - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. + When 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 contains JSON or YAML values specified directly in the ClusterExtension. - inline is used to specify arbitrary configuration values for the ClusterExtension. + It is used to specify arbitrary configuration values for the ClusterExtension. It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. The configuration values are validated at runtime against a JSON schema provided by the bundle. minProperties: 1 @@ -99,37 +97,35 @@ spec: : !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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -151,16 +147,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -170,26 +165,34 @@ spec: rule: self == oldSelf - message: namespace must be a valid DNS1123 label rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer serviceAccount: description: |- - serviceAccount is a reference to a ServiceAccount used to perform all interactions - with the cluster that are required to manage the extension. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -218,11 +221,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -233,30 +236,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -286,13 +288,12 @@ spec: 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 specifies 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. + It 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 @@ -319,12 +320,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -372,35 +370,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -478,13 +475,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -590,11 +586,13 @@ spec: x-kubernetes-list-type: map conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. @@ -603,12 +601,12 @@ spec: When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out. - 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -673,17 +671,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -693,8 +690,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver 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 index a0983e41f9..1840c756f1 100644 --- 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 @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clusterextensions.olm.operatorframework.io spec: @@ -57,39 +57,75 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: + config: + description: |- + config is optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. + properties: + configType: + description: |- + configType is required and specifies the type of configuration source. + + The only allowed value is "Inline". + + When 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. + + It is used to specify arbitrary configuration values for the ClusterExtension. + It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. + The configuration values are validated at runtime against a JSON schema provided by the bundle. + minProperties: 1 + 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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -111,16 +147,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -132,24 +167,22 @@ spec: 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. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -178,11 +211,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -193,30 +226,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -246,13 +278,12 @@ spec: 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 specifies 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. + It 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 @@ -279,12 +310,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -332,35 +360,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -438,13 +465,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -468,23 +494,25 @@ spec: properties: conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -549,17 +577,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -569,8 +596,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver diff --git a/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml b/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml index d6fec9b5fb..44c5bdea24 100644 --- a/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml +++ b/helm/olmv1/templates/e2e/configmap-olmv1-system-e2e-registries-conf.yml @@ -5,6 +5,10 @@ data: [[registry]] prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" + + [[registry]] + prefix = "dynamic-registry.operator-controller-e2e.svc.cluster.local:5000" + location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" kind: ConfigMap metadata: annotations: 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 index fa4b11acaa..ce1ff3c415 100644 --- 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 @@ -17,6 +17,7 @@ spec: image: busybox:1.36 name: tar securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: diff --git a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml b/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml deleted file mode 100644 index b8d6bcff48..0000000000 --- a/helm/olmv1/templates/openshift-catalogs/clustercatalog-openshift-redhat-marketplace.yml +++ /dev/null @@ -1,13 +0,0 @@ -{{- 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/tilt.yaml b/helm/tilt.yaml index aaed7c71fb..0fe3bec1f7 100644 --- a/helm/tilt.yaml +++ b/helm/tilt.yaml @@ -14,7 +14,6 @@ options: operatorController: features: enabled: - - SingleOwnNamespaceInstallSupport - PreflightPermissions - HelmChartSupport disabled: diff --git a/internal/catalogd/garbagecollection/garbage_collector_test.go b/internal/catalogd/garbagecollection/garbage_collector_test.go index dae428f6ee..5c5fbc6cdf 100644 --- a/internal/catalogd/garbagecollection/garbage_collector_test.go +++ b/internal/catalogd/garbagecollection/garbage_collector_test.go @@ -68,7 +68,7 @@ func TestRunGarbageCollection(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(cachePath, catalog.Name, "fakedigest"), os.ModePerm)) } - runtimeObjs := []runtime.Object{} + runtimeObjs := make([]runtime.Object, 0, len(tt.existCatalogs)) for _, catalog := range tt.existCatalogs { runtimeObjs = append(runtimeObjs, catalog) } diff --git a/internal/operator-controller/OWNERS b/internal/operator-controller/OWNERS deleted file mode 100644 index 3174715ba6..0000000000 --- a/internal/operator-controller/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -approvers: - - operator-controller-approvers diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go index c69ab5a1a0..5f10716c77 100644 --- a/internal/operator-controller/applier/boxcutter.go +++ b/internal/operator-controller/applier/boxcutter.go @@ -1,10 +1,12 @@ package applier import ( + "bytes" "cmp" "context" "errors" "fmt" + "io" "io/fs" "maps" "slices" @@ -17,6 +19,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/cli-runtime/pkg/printers" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -26,7 +31,9 @@ import ( 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/labels" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" "github.com/operator-framework/operator-controller/internal/shared/util/cache" ) @@ -60,12 +67,7 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease( if err := yaml.Unmarshal([]byte(doc), &obj); err != nil { return nil, err } - - existingLabels := obj.GetLabels() - labels := make(map[string]string, len(existingLabels)+len(objectLabels)) - maps.Copy(labels, existingLabels) - maps.Copy(labels, objectLabels) - obj.SetLabels(labels) + obj.SetLabels(mergeLabelMaps(obj.GetLabels(), objectLabels)) // Memory optimization: strip large annotations // Note: ApplyStripTransform never returns an error in practice @@ -100,14 +102,31 @@ func (r *SimpleRevisionGenerator) GenerateRevision( return nil, err } + if revisionAnnotations == nil { + revisionAnnotations = map[string]string{} + } + + // add bundle properties of interest to revision annotations + bundleAnnotations, err := getBundleAnnotations(bundleFS) + if err != nil { + return nil, fmt.Errorf("error getting bundle annotations: %w", err) + } + + // we don't care about all of the bundle and csv annotations as they can be quite confusing + // e.g. 'createdAt', 'capabilities', etc. + for _, key := range []string{ + // used by other operators that care about the bundle properties (e.g. maxOpenShiftVersion) + source.PropertyOLMProperties, + } { + if value, ok := bundleAnnotations[key]; ok { + revisionAnnotations[key] = value + } + } + // objectLabels objs := make([]ocv1.ClusterExtensionRevisionObject, 0, len(plain)) for _, obj := range plain { - existingLabels := obj.GetLabels() - labels := make(map[string]string, len(existingLabels)+len(objectLabels)) - maps.Copy(labels, existingLabels) - maps.Copy(labels, objectLabels) - obj.SetLabels(labels) + obj.SetLabels(mergeLabelMaps(obj.GetLabels(), objectLabels)) gvk, err := apiutil.GVKForObject(obj, r.Scheme) if err != nil { @@ -131,11 +150,6 @@ func (r *SimpleRevisionGenerator) GenerateRevision( Object: unstr, }) } - - if revisionAnnotations == nil { - revisionAnnotations = map[string]string{} - } - return r.buildClusterExtensionRevision(objs, ext, revisionAnnotations), nil } @@ -186,10 +200,17 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision( ext *ocv1.ClusterExtension, annotations map[string]string, ) *ocv1.ClusterExtensionRevision { - return &ocv1.ClusterExtensionRevision{ + if annotations == nil { + annotations = make(map[string]string) + } + annotations[labels.ServiceAccountNameKey] = ext.Spec.ServiceAccount.Name + annotations[labels.ServiceAccountNamespaceKey] = ext.Spec.Namespace + + cer := &ocv1.ClusterExtensionRevision{ ObjectMeta: metav1.ObjectMeta{ Annotations: annotations, Labels: map[string]string{ + labels.OwnerKindKey: ocv1.ClusterExtensionKind, labels.OwnerNameKey: ext.Name, }, }, @@ -201,6 +222,10 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision( Phases: PhaseSort(objects), }, } + if p := ext.Spec.ProgressDeadlineMinutes; p > 0 { + cer.Spec.ProgressDeadlineMinutes = p + } + return cer } // BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to @@ -229,8 +254,7 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste return fmt.Errorf("listing ClusterExtensionRevisions before attempting migration: %w", err) } if len(existingRevisionList.Items) != 0 { - // No migration needed. - return nil + return m.ensureMigratedRevisionStatus(ctx, existingRevisionList.Items) } ac, err := m.ActionClientGetter.ActionClientFor(ctx, ext) @@ -247,11 +271,36 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste return err } + // Only migrate from a Helm release that represents a deployed, working installation. + // If the latest revision is not deployed (e.g. FAILED), look through the history and + // select the most-recent deployed release instead. + if helmRelease == nil || helmRelease.Info == nil || helmRelease.Info.Status != release.StatusDeployed { + var err error + helmRelease, err = m.findLatestDeployedRelease(ac, ext.GetName()) + if err != nil { + return err + } + if helmRelease == nil { + // No deployed release found in history - skip migration. The ClusterExtension + // controller will handle this via normal rollout. + return nil + } + } + rev, err := m.RevisionGenerator.GenerateRevisionFromHelmRelease(ctx, helmRelease, ext, objectLabels) if err != nil { return err } + // Mark this revision as migrated from Helm so we can distinguish it from + // normal Boxcutter revisions. This label is critical for ensuring we only + // set Succeeded=True status on actually-migrated revisions, not on revision 1 + // created during normal Boxcutter operation. + if rev.Labels == nil { + rev.Labels = make(map[string]string) + } + rev.Labels[labels.MigratedFromHelmKey] = "true" + // Set ownerReference for proper garbage collection when the ClusterExtension is deleted. if err := controllerutil.SetControllerReference(ext, rev, m.Scheme); err != nil { return fmt.Errorf("set ownerref: %w", err) @@ -261,9 +310,105 @@ func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.Cluste return err } - // Re-fetch to get server-managed fields like Generation + // Set initial status on the migrated revision to mark it as succeeded. + // + // The revision must have a Succeeded=True status condition immediately after creation. + // + // A revision is only considered "Installed" (vs "RollingOut") when it has this condition. + // Without it, the system cannot determine what version is currently installed, which breaks: + // - Version resolution (can't compute upgrade paths from unknown starting point) + // - Status reporting (installed bundle appears as nil) + // - Subsequent upgrades (resolution fails without knowing current version) + // + // While the ClusterExtensionRevision controller would eventually reconcile and set this status, + // that creates a timing gap where the ClusterExtension reconciliation happens before the status + // is set, causing failures during the OLM upgrade window. + // + // Since we're creating this revision from a successfully deployed Helm release, we know it + // represents a working installation and can safely mark it as succeeded immediately. + return m.ensureRevisionStatus(ctx, rev) +} + +// ensureMigratedRevisionStatus checks if revision 1 exists and needs its status set. +// This handles the case where revision creation succeeded but status update failed. +// Returns nil if no action is needed. +func (m *BoxcutterStorageMigrator) ensureMigratedRevisionStatus(ctx context.Context, revisions []ocv1.ClusterExtensionRevision) error { + for i := range revisions { + if revisions[i].Spec.Revision != 1 { + continue + } + // Skip if already succeeded - status is already set correctly. + if meta.IsStatusConditionTrue(revisions[i].Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) { + return nil + } + // Ensure revision 1 status is set correctly, including for previously migrated + // revisions that may not carry the MigratedFromHelm label. + return m.ensureRevisionStatus(ctx, &revisions[i]) + } + // No revision 1 found - migration not applicable (revisions created by normal operation). + return nil +} + +// findLatestDeployedRelease searches the Helm release history for the most recent deployed release. +// Returns nil if no deployed release is found. +func (m *BoxcutterStorageMigrator) findLatestDeployedRelease(ac helmclient.ActionInterface, name string) (*release.Release, error) { + history, err := ac.History(name) + if errors.Is(err, driver.ErrReleaseNotFound) { + // no Helm Release history -> no prior installation. + return nil, nil + } + if err != nil { + return nil, err + } + + var latestDeployed *release.Release + for _, rel := range history { + if rel == nil || rel.Info == nil { + continue + } + if rel.Info.Status != release.StatusDeployed { + continue + } + if latestDeployed == nil || rel.Version > latestDeployed.Version { + latestDeployed = rel + } + } + + return latestDeployed, nil +} + +// ensureRevisionStatus ensures the revision has the Succeeded status condition set. +// Returns nil if the status is already set or after successfully setting it. +// Only sets status on revisions that were actually migrated from Helm (marked with MigratedFromHelmKey label). +func (m *BoxcutterStorageMigrator) ensureRevisionStatus(ctx context.Context, rev *ocv1.ClusterExtensionRevision) error { + // Re-fetch to get latest version before checking status if err := m.Client.Get(ctx, client.ObjectKeyFromObject(rev), rev); err != nil { - return fmt.Errorf("getting created revision: %w", err) + return fmt.Errorf("getting existing revision for status check: %w", err) + } + + // Only set status if this revision was actually migrated from Helm. + // This prevents us from incorrectly marking normal Boxcutter revision 1 as succeeded + // when it's still in progress. + if rev.Labels[labels.MigratedFromHelmKey] != "true" { + return nil + } + + // Check if status is already set to Succeeded=True + if meta.IsStatusConditionTrue(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) { + return nil + } + + // Set the Succeeded status condition + meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeSucceeded, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + Message: "Revision succeeded - migrated from Helm release", + ObservedGeneration: rev.GetGeneration(), + }) + + if err := m.Client.Status().Update(ctx, rev); err != nil { + return fmt.Errorf("updating migrated revision status: %w", err) } return nil @@ -274,35 +419,53 @@ type Boxcutter struct { Scheme *runtime.Scheme RevisionGenerator ClusterExtensionRevisionGenerator Preflights []Preflight + PreAuthorizer authorization.PreAuthorizer 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) +// createOrUpdate creates or updates the revision object. PreAuthorization checks are performed to ensure the +// user has sufficient permissions to manage the revision and its resources +func (bc *Boxcutter) createOrUpdate(ctx context.Context, user user.Info, rev *ocv1.ClusterExtensionRevision) error { + if rev.GetObjectKind().GroupVersionKind().Empty() { + gvk, err := apiutil.GVKForObject(rev, bc.Scheme) + if err != nil { + return err } + rev.GetObjectKind().SetGroupVersionKind(gvk) } - return objs + + // Run auth preflight checks + if err := bc.runPreAuthorizationChecks(ctx, user, rev); err != nil { + return err + } + + return bc.Client.Patch(ctx, rev, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership) } -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 +func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) { + // List existing revisions first to validate cluster connectivity before checking contentFS. + // This ensures we fail fast on API errors rather than attempting fallback behavior when + // cluster access is unavailable (since the ClusterExtensionRevision controller also requires + // API access to maintain resources). The revision list is also needed to determine if fallback + // is possible when contentFS is nil (at least one revision must exist). + existingRevisions, err := bc.getExistingRevisions(ctx, ext.GetName()) + if err != nil { + return false, "", err + } + + // If contentFS is nil, we're maintaining the current state without catalog access. + // In this case, we should use the existing installed revision without generating a new one. + if contentFS == nil { + if len(existingRevisions) == 0 { + return false, "", fmt.Errorf("catalog content unavailable and no revision installed") } - obj.GetObjectKind().SetGroupVersionKind(gvk) + // Returning true here signals that the rollout has succeeded using the current revision. + // This assumes the ClusterExtensionRevision controller is running and will continue to + // reconcile, apply, and maintain the resources defined in that revision via Server-Side Apply, + // ensuring the workload keeps running even when catalog access is unavailable. + return true, "", nil } - 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(ctx, contentFS, ext, objectLabels, revisionAnnotations) if err != nil { @@ -313,12 +476,6 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust 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. @@ -328,7 +485,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust desiredRevision.Spec.Revision = currentRevision.Spec.Revision desiredRevision.Name = currentRevision.Name - err := bc.createOrUpdate(ctx, desiredRevision) + err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision) switch { case apierrors.IsInvalid(err): // We could not update the current revision due to trying to update an immutable field. @@ -343,7 +500,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust } // Preflights - plainObjs := bc.getObjects(desiredRevision) + plainObjs := getObjects(desiredRevision) for _, preflight := range bc.Preflights { if shouldSkipPreflight(ctx, preflight, ext, state) { continue @@ -378,31 +535,29 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust return false, "", fmt.Errorf("garbage collecting old revisions: %w", err) } - if err := bc.createOrUpdate(ctx, desiredRevision); err != nil { + if err := bc.createOrUpdate(ctx, getUserInfo(ext), 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) + return true, "", nil +} - if progressingCondition == nil && availableCondition == nil && succeededCondition == nil { - return false, "New revision created", nil - } else if progressingCondition != nil && progressingCondition.Status == metav1.ConditionTrue { - switch progressingCondition.Reason { - case ocv1.ReasonSucceeded: - return true, "", nil - case ocv1.ClusterExtensionRevisionReasonRetrying: - return false, "", errors.New(progressingCondition.Message) - default: - return false, progressingCondition.Message, nil - } - } else if succeededCondition != nil && succeededCondition.Status != metav1.ConditionTrue { - return false, succeededCondition.Message, nil +// runPreAuthorizationChecks runs PreAuthorization checks if the PreAuthorizer is set. An error will be returned if +// the ClusterExtension service account does not have the necessary permissions to manage the revision's resources +func (bc *Boxcutter) runPreAuthorizationChecks(ctx context.Context, user user.Info, rev *ocv1.ClusterExtensionRevision) error { + if bc.PreAuthorizer == nil { + return nil } - return true, "", nil + + // collect the revision manifests + manifestReader, err := revisionManifestReader(rev) + if err != nil { + return err + } + + // run preauthorization check + return formatPreAuthorizerOutput(bc.PreAuthorizer.PreAuthorize(ctx, user, manifestReader, revisionManagementPerms(rev))) } // garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit. @@ -463,3 +618,54 @@ func splitManifestDocuments(file string) []string { } return docs } + +// getObjects returns a slice of all objects in the revision +func getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object { + totalObjects := 0 + for _, phase := range rev.Spec.Phases { + totalObjects += len(phase.Objects) + } + objs := make([]client.Object, 0, totalObjects) + for _, phase := range rev.Spec.Phases { + for _, phaseObject := range phase.Objects { + objs = append(objs, &phaseObject.Object) + } + } + return objs +} + +// revisionManifestReader returns an io.Reader containing all manifests in the revision +func revisionManifestReader(rev *ocv1.ClusterExtensionRevision) (io.Reader, error) { + printer := printers.YAMLPrinter{} + buf := new(bytes.Buffer) + for _, obj := range getObjects(rev) { + buf.WriteString("---\n") + if err := printer.PrintObj(obj, buf); err != nil { + return nil, err + } + } + return buf, nil +} + +func revisionManagementPerms(rev *ocv1.ClusterExtensionRevision) func(user.Info) []authorizer.AttributesRecord { + return func(user user.Info) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: user, + Name: rev.Name, + APIGroup: ocv1.GroupVersion.Group, + APIVersion: ocv1.GroupVersion.Version, + Resource: "clusterextensionrevisions/finalizers", + ResourceRequest: true, + Verb: "update", + }, + } + } +} + +func mergeLabelMaps(m1, m2 map[string]string) map[string]string { + mergedLabels := make(map[string]string, len(m1)+len(m2)) + maps.Copy(mergedLabels, m1) + maps.Copy(mergedLabels, m2) + return mergedLabels +} diff --git a/internal/operator-controller/applier/boxcutter_test.go b/internal/operator-controller/applier/boxcutter_test.go index 8cae359f51..f7c1298cea 100644 --- a/internal/operator-controller/applier/boxcutter_test.go +++ b/internal/operator-controller/applier/boxcutter_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "io/fs" "strings" "testing" @@ -16,19 +17,34 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apierrors "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/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" k8scheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" "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/authorization" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" + "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" +) + +var ( + dummyBundle = bundlefs.Builder(). + WithPackageName("test-package"). + WithCSV(clusterserviceversion.Builder().WithName("test-csv").Build()). + Build() ) func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T) { @@ -66,6 +82,12 @@ func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T) ObjectMeta: metav1.ObjectMeta{ Name: "test-123", }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "test-sa", + }, + }, } objectLabels := map[string]string{ @@ -79,12 +101,15 @@ func Test_SimpleRevisionGenerator_GenerateRevisionFromHelmRelease(t *testing.T) 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", + "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", + "olm.operatorframework.io/service-account-name": "test-sa", + "olm.operatorframework.io/service-account-namespace": "test-namespace", }, Labels: map[string]string{ + labels.OwnerKindKey: ocv1.ClusterExtensionKind, labels.OwnerNameKey: "test-123", }, }, @@ -172,13 +197,20 @@ func Test_SimpleRevisionGenerator_GenerateRevision(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "test-extension", }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "test-sa", + }, + }, } - rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, ext, map[string]string{}, map[string]string{}) + rev, err := b.GenerateRevision(t.Context(), dummyBundle, ext, map[string]string{}, map[string]string{}) require.NoError(t, err) - t.Log("by checking the olm.operatorframework.io/owner-name label is set to the name of the ClusterExtension") + t.Log("by checking the olm.operatorframework.io/owner-name and owner-kind labels are set") require.Equal(t, map[string]string{ + labels.OwnerKindKey: ocv1.ClusterExtensionKind, labels.OwnerNameKey: "test-extension", }, rev.Labels) t.Log("by checking the revision number is 0") @@ -233,8 +265,88 @@ func Test_SimpleRevisionGenerator_GenerateRevision(t *testing.T) { }, rev.Spec.Phases) } +func Test_SimpleRevisionGenerator_GenerateRevision_BundleAnnotations(t *testing.T) { + r := &FakeManifestProvider{ + GetFn: func(_ fs.FS, _ *ocv1.ClusterExtension) ([]client.Object, error) { + return []client.Object{}, nil + }, + } + + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "test-sa", + }, + }, + } + + t.Run("bundle properties are copied to the olm.properties annotation", func(t *testing.T) { + bundleFS := bundlefs.Builder(). + WithPackageName("test-package"). + WithBundleProperty("olm.bundle.property", "some-value"). + WithBundleProperty("olm.another.bundle.property", "some-other-value"). + WithCSV(clusterserviceversion.Builder().WithName("test-csv").Build()). + Build() + + rev, err := b.GenerateRevision(t.Context(), bundleFS, ext, map[string]string{}, map[string]string{}) + require.NoError(t, err) + + t.Log("by checking bundle properties are added to the revision annotations") + require.NotNil(t, rev.Annotations) + require.JSONEq(t, `[{"type":"olm.bundle.property","value":"some-value"},{"type":"olm.another.bundle.property","value":"some-other-value"}]`, rev.Annotations["olm.properties"]) + }) + + t.Run("olm.properties should not be present if there are no bundle properties", func(t *testing.T) { + bundleFS := bundlefs.Builder(). + WithPackageName("test-package"). + WithCSV(clusterserviceversion.Builder().WithName("test-csv").Build()). + Build() + + rev, err := b.GenerateRevision(t.Context(), bundleFS, ext, map[string]string{}, map[string]string{}) + require.NoError(t, err) + + t.Log("by checking olm.properties is not present in the revision annotations") + _, ok := rev.Annotations["olm.properties"] + require.False(t, ok, "olm.properties should not be present in the revision annotations") + }) + + t.Run("csv annotations are not added to the revision annotations", func(t *testing.T) { + bundleFS := bundlefs.Builder(). + WithPackageName("test-package"). + WithBundleProperty("olm.bundle.property", "some-value"). + WithCSV(clusterserviceversion.Builder(). + WithName("test-csv"). + WithAnnotations(map[string]string{ + "some.csv.annotation": "some-other-value", + }). + Build()). + Build() + + rev, err := b.GenerateRevision(t.Context(), bundleFS, ext, map[string]string{}, map[string]string{}) + require.NoError(t, err) + + t.Log("by checking csv annotations are not added to the revision annotations") + _, ok := rev.Annotations["olm.csv.annotation"] + require.False(t, ok, "csv annotation should not be present in the revision annotations") + }) + + t.Run("errors getting bundle properties are surfaced", func(t *testing.T) { + _, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, ext, map[string]string{}, map[string]string{}) + require.Error(t, err) + require.Contains(t, err.Error(), "metadata/annotations.yaml: file does not exist") + }) +} + func Test_SimpleRevisionGenerator_Renderer_Integration(t *testing.T) { - bundleFS := fstest.MapFS{} ext := &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Name: "test-extension", @@ -243,7 +355,7 @@ func Test_SimpleRevisionGenerator_Renderer_Integration(t *testing.T) { 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, dummyBundle, b) require.Equal(t, ext, e) return nil, nil }, @@ -253,7 +365,7 @@ func Test_SimpleRevisionGenerator_Renderer_Integration(t *testing.T) { ManifestProvider: r, } - _, err := b.GenerateRevision(t.Context(), bundleFS, ext, map[string]string{}, map[string]string{}) + _, err := b.GenerateRevision(t.Context(), dummyBundle, ext, map[string]string{}, map[string]string{}) require.NoError(t, err) } @@ -291,7 +403,12 @@ func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *t "other": "value", } - rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{ + rev, err := b.GenerateRevision(t.Context(), dummyBundle, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"}, + }, + }, map[string]string{ "some": "value", }, revAnnotations) require.NoError(t, err) @@ -308,6 +425,65 @@ func Test_SimpleRevisionGenerator_AppliesObjectLabelsAndRevisionAnnotations(t *t require.Equal(t, revAnnotations, rev.Annotations) } +func Test_SimpleRevisionGenerator_PropagatesProgressDeadlineMinutes(t *testing.T) { + r := &FakeManifestProvider{ + GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) { + return []client.Object{}, nil + }, + } + + b := applier.SimpleRevisionGenerator{ + Scheme: k8scheme.Scheme, + ManifestProvider: r, + } + + type args struct { + progressDeadlineMinutes *int32 + } + type want struct { + progressDeadlineMinutes int32 + } + type testCase struct { + args args + want want + } + for name, tc := range map[string]testCase{ + "propagates when set": { + args: args{ + progressDeadlineMinutes: ptr.To(int32(10)), + }, + want: want{ + progressDeadlineMinutes: 10, + }, + }, + "do not propagate when unset": { + want: want{ + progressDeadlineMinutes: 0, + }, + }, + } { + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"}, + }, + } + empty := map[string]string{} + t.Run(name, func(t *testing.T) { + if pd := tc.args.progressDeadlineMinutes; pd != nil { + ext.Spec.ProgressDeadlineMinutes = *pd + } + + rev, err := b.GenerateRevision(t.Context(), dummyBundle, ext, empty, empty) + require.NoError(t, err) + require.Equal(t, tc.want.progressDeadlineMinutes, rev.Spec.ProgressDeadlineMinutes) + }) + } +} + func Test_SimpleRevisionGenerator_Failure(t *testing.T) { r := &FakeManifestProvider{ GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) { @@ -319,7 +495,12 @@ func Test_SimpleRevisionGenerator_Failure(t *testing.T) { ManifestProvider: r, } - rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{}, map[string]string{}, map[string]string{}) + rev, err := b.GenerateRevision(t.Context(), fstest.MapFS{}, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"}, + }, + }, map[string]string{}, map[string]string{}) require.Nil(t, rev) t.Log("by checking rendering errors are propagated") require.Error(t, err) @@ -902,18 +1083,18 @@ func TestBoxcutter_Apply(t *testing.T) { labels.PackageNameKey: "test-package", } } - installSucceeded, installStatus, err := boxcutter.Apply(t.Context(), testFS, ext, nil, revisionAnnotations) + completed, status, err := boxcutter.Apply(t.Context(), testFS, ext, nil, revisionAnnotations) // Assert if tc.expectedErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) - assert.False(t, installSucceeded) - assert.Empty(t, installStatus) + assert.False(t, completed) + assert.Empty(t, status) } else { require.NoError(t, err) - assert.False(t, installSucceeded) - assert.Equal(t, "New revision created", installStatus) + assert.True(t, completed) + assert.Empty(t, status) } if tc.validate != nil { @@ -928,13 +1109,205 @@ func TestBoxcutter_Apply(t *testing.T) { } } +func Test_PreAuthorizer_Integration(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", + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "test-sa", + }, + }, + } + fakeClient := fake.NewClientBuilder().WithScheme(testScheme).Build() + dummyGenerator := &mockBundleRevisionBuilder{ + makeRevisionFunc: func(ctx context.Context, bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotation map[string]string) (*ocv1.ClusterExtensionRevision, error) { + return &ocv1.ClusterExtensionRevision{ + Spec: ocv1.ClusterExtensionRevisionSpec{ + Phases: []ocv1.ClusterExtensionRevisionPhase{ + { + Name: "some-phase", + Objects: []ocv1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": map[string]string{ + "test-data": "test-data", + }, + }, + }, + }, + }, + }, + }, + }, + }, nil + }, + } + dummyBundleFs := fstest.MapFS{} + revisionAnnotations := map[string]string{} + + for _, tc := range []struct { + name string + preAuthorizer func(t *testing.T) authorization.PreAuthorizer + validate func(t *testing.T, err error) + }{ + { + name: "preauthorizer called with correct parameters", + preAuthorizer: func(t *testing.T) authorization.PreAuthorizer { + return &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + require.Equal(t, "system:serviceaccount:test-namespace:test-sa", user.GetName()) + require.Empty(t, user.GetUID()) + require.Nil(t, user.GetExtra()) + require.Empty(t, user.GetGroups()) + + t.Log("has correct additional permissions") + require.Len(t, additionalRequiredPerms, 1) + perms := additionalRequiredPerms[0](user) + + require.Len(t, perms, 1) + require.Equal(t, authorizer.AttributesRecord{ + User: user, + Name: "test-ext-1", + APIGroup: "olm.operatorframework.io", + APIVersion: "v1", + Resource: "clusterextensionrevisions/finalizers", + ResourceRequest: true, + Verb: "update", + }, perms[0]) + + t.Log("has correct manifest reader") + manifests, err := io.ReadAll(reader) + require.NoError(t, err) + require.Equal(t, "---\napiVersion: v1\ndata:\n test-data: test-data\nkind: ConfigMap\n", string(manifests)) + return nil, nil + }, + } + }, + }, { + name: "preauthorizer errors are returned", + preAuthorizer: func(t *testing.T) authorization.PreAuthorizer { + return &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return nil, errors.New("test error") + }, + } + }, + validate: func(t *testing.T, err error) { + require.Error(t, err) + require.Contains(t, err.Error(), "pre-authorization failed") + require.Contains(t, err.Error(), "authorization evaluation error: test error") + }, + }, { + name: "preauthorizer missing permissions are returned as an error", + preAuthorizer: func(t *testing.T) authorization.PreAuthorizer { + return &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return []authorization.ScopedPolicyRules{ + { + Namespace: "", + MissingRules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + }, nil + }, + } + }, + validate: func(t *testing.T, err error) { + require.Error(t, err) + require.Contains(t, err.Error(), "pre-authorization failed") + require.Contains(t, err.Error(), "service account requires the following permissions") + require.Contains(t, err.Error(), "Resources:[pods]") + require.Contains(t, err.Error(), "Verbs:[get,list,watch]") + }, + }, { + name: "preauthorizer missing permissions and errors are combined and returned as an error", + preAuthorizer: func(t *testing.T) authorization.PreAuthorizer { + return &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return []authorization.ScopedPolicyRules{ + { + Namespace: "", + MissingRules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + }, errors.New("test error") + }, + } + }, + validate: func(t *testing.T, err error) { + require.Error(t, err) + require.Contains(t, err.Error(), "pre-authorization failed") + require.Contains(t, err.Error(), "service account requires the following permissions") + require.Contains(t, err.Error(), "Resources:[pods]") + require.Contains(t, err.Error(), "Verbs:[get,list,watch]") + require.Contains(t, err.Error(), "authorization evaluation error: test error") + }, + }, { + name: "successful call to preauthorizer does not block applier", + preAuthorizer: func(t *testing.T) authorization.PreAuthorizer { + return &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return nil, nil + }, + } + }, + validate: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + boxcutter := &applier.Boxcutter{ + Client: fakeClient, + Scheme: testScheme, + FieldOwner: "test-owner", + RevisionGenerator: dummyGenerator, + PreAuthorizer: tc.preAuthorizer(t), + } + completed, status, err := boxcutter.Apply(t.Context(), dummyBundleFs, ext, nil, revisionAnnotations) + if tc.validate != nil { + tc.validate(t, err) + } + _ = completed + _ = status + }) + } +} + func TestBoxcutterStorageMigrator(t *testing.T) { t.Run("creates revision", func(t *testing.T) { testScheme := runtime.NewScheme() require.NoError(t, ocv1.AddToScheme(testScheme)) brb := &mockBundleRevisionBuilder{} - mag := &mockActionGetter{} + mag := &mockActionGetter{ + currentRel: &release.Release{ + Name: "test123", + Info: &release.Info{Status: release.StatusDeployed}, + }, + } client := &clientMock{} sm := &applier.BoxcutterStorageMigrator{ RevisionGenerator: brb, @@ -954,8 +1327,11 @@ func TestBoxcutterStorageMigrator(t *testing.T) { On("Create", mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). Once(). Run(func(args mock.Arguments) { - // Simulate real Kubernetes behavior: Create() populates server-managed fields + // Verify the migration marker label is set before creation rev := args.Get(1).(*ocv1.ClusterExtensionRevision) + require.Equal(t, "true", rev.Labels[labels.MigratedFromHelmKey], "Migration marker label should be set") + + // Simulate real Kubernetes behavior: Create() populates server-managed fields rev.Generation = 1 rev.ResourceVersion = "1" }). @@ -975,6 +1351,21 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, err) client.AssertExpectations(t) + + // Verify the migrated revision has Succeeded=True status with Succeeded reason and a migration message + statusWriter := client.Status().(*statusWriterMock) + require.True(t, statusWriter.updateCalled, "Status().Update() should be called during migration") + require.NotNil(t, statusWriter.updatedObj, "Updated object should not be nil") + + rev, ok := statusWriter.updatedObj.(*ocv1.ClusterExtensionRevision) + require.True(t, ok, "Updated object should be a ClusterExtensionRevision") + + succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + require.NotNil(t, succeededCond, "Succeeded condition should be set") + assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True") + assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded") + assert.Equal(t, "Revision succeeded - migrated from Helm release", succeededCond.Message, "Message should indicate Helm migration") + assert.Equal(t, int64(1), succeededCond.ObservedGeneration, "ObservedGeneration should match revision generation") }) t.Run("does not create revision when revisions exist", func(t *testing.T) { @@ -995,12 +1386,313 @@ func TestBoxcutterStorageMigrator(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "test123"}, } + existingRev := ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-revision", + Generation: 2, + Labels: map[string]string{ + labels.MigratedFromHelmKey: "true", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, // Migration creates revision 1 + }, + Status: ocv1.ClusterExtensionRevisionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeSucceeded, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + }, + }, + }, + } + 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. + list.Items = []ocv1.ClusterExtensionRevision{existingRev} + }). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + }) + + t.Run("sets status when revision exists but status is missing", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{} + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + Scheme: testScheme, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + existingRev := ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-revision", + Generation: 2, + Labels: map[string]string{ + labels.MigratedFromHelmKey: "true", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, // Migration creates revision 1 + }, + // Status is empty - simulating the case where creation succeeded but status update failed + } + + 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{existingRev} + }). + Return(nil) + + client. + On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). + Run(func(args mock.Arguments) { + rev := args.Get(2).(*ocv1.ClusterExtensionRevision) + *rev = existingRev + }). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + + // Verify the status was set + statusWriter := client.Status().(*statusWriterMock) + require.True(t, statusWriter.updateCalled, "Status().Update() should be called to set missing status") + require.NotNil(t, statusWriter.updatedObj, "Updated object should not be nil") + + rev, ok := statusWriter.updatedObj.(*ocv1.ClusterExtensionRevision) + require.True(t, ok, "Updated object should be a ClusterExtensionRevision") + + succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + require.NotNil(t, succeededCond, "Succeeded condition should be set") + assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True") + assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded") + }) + + t.Run("updates status from False to True for migrated revision", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{} + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + Scheme: testScheme, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + // Migrated revision with Succeeded=False (e.g., from a previous failed status update attempt) + // This simulates a revision whose Succeeded condition should be corrected from False to True during migration. + existingRev := ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-revision", + Generation: 2, + Labels: map[string]string{ + labels.MigratedFromHelmKey: "true", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, + }, + Status: ocv1.ClusterExtensionRevisionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeSucceeded, + Status: metav1.ConditionFalse, // Important: False, not missing + Reason: "InProgress", + }, + }, + }, + } + + 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{existingRev} + }). + Return(nil) + + client. + On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). + Run(func(args mock.Arguments) { + rev := args.Get(2).(*ocv1.ClusterExtensionRevision) + *rev = existingRev + }). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + + // Verify the status was updated from False to True + statusWriter := client.Status().(*statusWriterMock) + require.True(t, statusWriter.updateCalled, "Status().Update() should be called to update False to True") + require.NotNil(t, statusWriter.updatedObj, "Updated object should not be nil") + + rev, ok := statusWriter.updatedObj.(*ocv1.ClusterExtensionRevision) + require.True(t, ok, "Updated object should be a ClusterExtensionRevision") + + succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + require.NotNil(t, succeededCond, "Succeeded condition should be set") + assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be updated to True") + assert.Equal(t, ocv1.ReasonSucceeded, succeededCond.Reason, "Reason should be Succeeded") + }) + + t.Run("does not set status on non-migrated revision 1", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{} + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + Scheme: testScheme, + } + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + + // Revision 1 created by normal Boxcutter operation (no migration label) + // This simulates the first rollout - status should NOT be set as it may still be in progress + existingRev := ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-revision", + Generation: 2, + // No migration label - this is a normal Boxcutter revision + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + Revision: 1, + }, + } + + 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{existingRev} + }). + Return(nil) + + // The migration flow calls Get() to re-fetch the revision before checking its status. + // Even for non-migrated revisions, Get() is called to determine if status needs to be set. + client. + On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). + Run(func(args mock.Arguments) { + rev := args.Get(2).(*ocv1.ClusterExtensionRevision) + *rev = existingRev + }). + Return(nil) + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + + client.AssertExpectations(t) + + // Verify the status was NOT set for non-migrated revision + statusWriter := client.Status().(*statusWriterMock) + require.False(t, statusWriter.updateCalled, "Status().Update() should NOT be called for non-migrated revisions") + }) + + t.Run("migrates from most recent deployed release when latest is failed", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{ + currentRel: &release.Release{ + Name: "test123", + Version: 3, + Info: &release.Info{Status: release.StatusFailed}, + }, + history: []*release.Release{ + { + Name: "test123", + Version: 3, + Info: &release.Info{Status: release.StatusFailed}, + }, + { + Name: "test123", + Version: 2, + Info: &release.Info{Status: release.StatusDeployed}, + }, + { + Name: "test123", + Version: 1, + Info: &release.Info{Status: release.StatusSuperseded}, + }, + }, + } + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + Scheme: testScheme, + } + + 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(). + Run(func(args mock.Arguments) { + // Verify the migration marker label is set before creation + rev := args.Get(1).(*ocv1.ClusterExtensionRevision) + require.Equal(t, "true", rev.Labels[labels.MigratedFromHelmKey], "Migration marker label should be set") + + // Simulate real Kubernetes behavior: Create() populates server-managed fields + rev.Generation = 1 + rev.ResourceVersion = "1" + }). + Return(nil) + + client. + On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1.ClusterExtensionRevision"), mock.Anything). + Run(func(args mock.Arguments) { + rev := args.Get(2).(*ocv1.ClusterExtensionRevision) + rev.ObjectMeta.Name = "test-revision" + rev.ObjectMeta.Generation = 1 + rev.ObjectMeta.ResourceVersion = "1" + rev.Labels = map[string]string{ + labels.MigratedFromHelmKey: "true", } }). Return(nil) @@ -1009,6 +1701,70 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, err) client.AssertExpectations(t) + + // Verify the correct release (version 2, deployed) was used instead of version 3 (failed) + require.NotNil(t, brb.helmReleaseUsed, "GenerateRevisionFromHelmRelease should have been called") + assert.Equal(t, 2, brb.helmReleaseUsed.Version, "Should use version 2 (deployed), not version 3 (failed)") + assert.Equal(t, release.StatusDeployed, brb.helmReleaseUsed.Info.Status, "Should use deployed release") + + // Verify the migrated revision has Succeeded=True status + statusWriter := client.Status().(*statusWriterMock) + require.True(t, statusWriter.updateCalled, "Status().Update() should be called during migration") + require.NotNil(t, statusWriter.updatedObj, "Updated object should not be nil") + + rev, ok := statusWriter.updatedObj.(*ocv1.ClusterExtensionRevision) + require.True(t, ok, "Updated object should be a ClusterExtensionRevision") + + succeededCond := apimeta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + require.NotNil(t, succeededCond, "Succeeded condition should be set") + assert.Equal(t, metav1.ConditionTrue, succeededCond.Status, "Succeeded condition should be True") + }) + + t.Run("does not create revision when helm release is not deployed and no deployed history", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + brb := &mockBundleRevisionBuilder{} + mag := &mockActionGetter{ + currentRel: &release.Release{ + Name: "test123", + Info: &release.Info{Status: release.StatusFailed}, + }, + history: []*release.Release{ + { + Name: "test123", + Version: 2, + Info: &release.Info{Status: release.StatusFailed}, + }, + { + Name: "test123", + Version: 1, + Info: &release.Info{Status: release.StatusFailed}, + }, + }, + } + client := &clientMock{} + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: client, + Scheme: testScheme, + } + + 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) + // brb.GenerateRevisionFromHelmRelease should NOT have been called + require.False(t, brb.generateRevisionFromHelmReleaseCalled, "GenerateRevisionFromHelmRelease should NOT be called when no deployed release exists") }) t.Run("does not create revision when no helm release", func(t *testing.T) { @@ -1044,7 +1800,9 @@ func TestBoxcutterStorageMigrator(t *testing.T) { // mockBundleRevisionBuilder is a mock implementation of the ClusterExtensionRevisionGenerator for testing. type mockBundleRevisionBuilder struct { - makeRevisionFunc func(ctx context.Context, bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotation map[string]string) (*ocv1.ClusterExtensionRevision, error) + makeRevisionFunc func(ctx context.Context, bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotation map[string]string) (*ocv1.ClusterExtensionRevision, error) + generateRevisionFromHelmReleaseCalled bool + helmReleaseUsed *release.Release } func (m *mockBundleRevisionBuilder) GenerateRevision(ctx context.Context, bundleFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (*ocv1.ClusterExtensionRevision, error) { @@ -1056,6 +1814,8 @@ func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease( helmRelease *release.Release, ext *ocv1.ClusterExtension, objectLabels map[string]string, ) (*ocv1.ClusterExtensionRevision, error) { + m.generateRevisionFromHelmReleaseCalled = true + m.helmReleaseUsed = helmRelease return &ocv1.ClusterExtensionRevision{ ObjectMeta: metav1.ObjectMeta{ Name: "test-revision", @@ -1069,6 +1829,7 @@ func (m *mockBundleRevisionBuilder) GenerateRevisionFromHelmRelease( type clientMock struct { mock.Mock + statusWriter *statusWriterMock } func (m *clientMock) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { @@ -1087,15 +1848,22 @@ func (m *clientMock) Create(ctx context.Context, obj client.Object, opts ...clie } func (m *clientMock) Status() client.StatusWriter { - return &statusWriterMock{mock: &m.Mock} + if m.statusWriter == nil { + m.statusWriter = &statusWriterMock{mock: &m.Mock} + } + return m.statusWriter } type statusWriterMock struct { - mock *mock.Mock + mock *mock.Mock + updatedObj client.Object + updateCalled bool } func (s *statusWriterMock) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { - // Status updates are expected during migration - return success by default + // Capture the status update for test verification + s.updatedObj = obj + s.updateCalled = true return nil } diff --git a/internal/operator-controller/applier/helm.go b/internal/operator-controller/applier/helm.go index 22ed096fe1..ab6e58a363 100644 --- a/internal/operator-controller/applier/helm.go +++ b/internal/operator-controller/applier/helm.go @@ -19,6 +19,8 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apimachyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -77,32 +79,21 @@ func (h *Helm) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterE return fmt.Errorf("error rendering content for pre-authorization checks: %w", err) } - missingRules, authErr := h.PreAuthorizer.PreAuthorize(ctx, ext, strings.NewReader(tmplRel.Manifest)) - - var preAuthErrors []error + manifestManager := getUserInfo(ext) + return formatPreAuthorizerOutput(h.PreAuthorizer.PreAuthorize(ctx, manifestManager, strings.NewReader(tmplRel.Manifest), extManagementPerms(ext))) +} - if len(missingRules) > 0 { - var missingRuleDescriptions []string - for _, policyRules := range missingRules { - for _, rule := range policyRules.MissingRules { - missingRuleDescriptions = append(missingRuleDescriptions, ruleDescription(policyRules.Namespace, rule)) - } +func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) (bool, string, error) { + // If contentFS is nil, we're maintaining the current state without catalog access. + // In this case, reconcile the existing Helm release if it exists. + if contentFS == nil { + ac, err := h.ActionClientGetter.ActionClientFor(ctx, ext) + if err != nil { + return false, "", err } - slices.Sort(missingRuleDescriptions) - // This phrase is explicitly checked by external testing - preAuthErrors = append(preAuthErrors, fmt.Errorf("service account requires the following permissions to manage cluster extension:\n %s", strings.Join(missingRuleDescriptions, "\n "))) - } - if authErr != nil { - preAuthErrors = append(preAuthErrors, fmt.Errorf("authorization evaluation error: %w", authErr)) - } - if len(preAuthErrors) > 0 { - // This phrase is explicitly checked by external testing - return fmt.Errorf("pre-authorization failed: %v", errors.Join(preAuthErrors...)) + return h.reconcileExistingRelease(ctx, ac, ext) } - return nil -} -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 false, "", err @@ -197,6 +188,62 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte return true, "", nil } +// reconcileExistingRelease reconciles an existing Helm release without catalog access. +// This is used when the catalog is unavailable but we need to maintain the current installation. +// It reconciles the release to actively maintain resources, and sets up watchers for monitoring/observability. +func (h *Helm) reconcileExistingRelease(ctx context.Context, ac helmclient.ActionInterface, ext *ocv1.ClusterExtension) (bool, string, error) { + rel, err := ac.Get(ext.GetName()) + if errors.Is(err, driver.ErrReleaseNotFound) { + return false, "", fmt.Errorf("catalog content unavailable and no release installed") + } + if err != nil { + return false, "", fmt.Errorf("failed to get current release: %w", err) + } + + // Reconcile the existing release to ensure resources are maintained + if err := ac.Reconcile(rel); err != nil { + // Reconcile failed - resources NOT maintained + // Return false (rollout failed) with error + return false, "", err + } + + // At this point: Reconcile succeeded - resources ARE maintained (applied to cluster via Server-Side Apply) + // The operations below are for setting up watches to detect drift (i.e., if someone manually modifies the + // resources). If watch setup fails, the resources are still successfully maintained, but we won't detect + // and auto-correct manual modifications. We return true (rollout succeeded) and log watch errors. + logger := klog.FromContext(ctx) + + relObjects, err := util.ManifestObjects(strings.NewReader(rel.Manifest), fmt.Sprintf("%s-release-manifest", rel.Name)) + if err != nil { + logger.Error(err, "failed to parse manifest objects, cannot set up drift detection watches (resources are applied but drift detection disabled)") + return true, "", nil + } + + logger.V(1).Info("setting up drift detection watches on managed objects") + + // Defensive nil checks to prevent panics if Manager or Watcher not properly initialized + if h.Manager == nil { + logger.Error(fmt.Errorf("manager is nil"), "Manager not initialized, cannot set up drift detection watches (resources are applied but drift detection disabled)") + return true, "", nil + } + cache, err := h.Manager.Get(ctx, ext) + if err != nil { + logger.Error(err, "failed to get managed content cache, cannot set up drift detection watches (resources are applied but drift detection disabled)") + return true, "", nil + } + + if h.Watcher == nil { + logger.Error(fmt.Errorf("watcher is nil"), "Watcher not initialized, cannot set up drift detection watches (resources are applied but drift detection disabled)") + return true, "", nil + } + if err := cache.Watch(ctx, h.Watcher, relObjects...); err != nil { + logger.Error(err, "failed to set up drift detection watches (resources are applied but drift detection disabled)") + return true, "", nil + } + + return true, "", nil +} + func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*chart.Chart, error) { if h.HelmChartProvider == nil { return nil, errors.New("HelmChartProvider is nil") @@ -328,3 +375,52 @@ func ruleDescription(ns string, rule rbacv1.PolicyRule) string { } return sb.String() } + +// formatPreAuthorizerOutput formats the output of PreAuthorizer.PreAuthorize calls into a consistent and deterministic error. +// If the call returns no missing rules, and no error, nil is returned. +func formatPreAuthorizerOutput(missingRules []authorization.ScopedPolicyRules, authErr error) error { + var preAuthErrors []error + if len(missingRules) > 0 { + totalMissingRules := 0 + for _, policyRules := range missingRules { + totalMissingRules += len(policyRules.MissingRules) + } + missingRuleDescriptions := make([]string, 0, totalMissingRules) + for _, policyRules := range missingRules { + for _, rule := range policyRules.MissingRules { + missingRuleDescriptions = append(missingRuleDescriptions, ruleDescription(policyRules.Namespace, rule)) + } + } + slices.Sort(missingRuleDescriptions) + // This phrase is explicitly checked by external testing + preAuthErrors = append(preAuthErrors, fmt.Errorf("service account requires the following permissions to manage cluster extension:\n %s", strings.Join(missingRuleDescriptions, "\n "))) + } + if authErr != nil { + preAuthErrors = append(preAuthErrors, fmt.Errorf("authorization evaluation error: %w", authErr)) + } + if len(preAuthErrors) > 0 { + // This phrase is explicitly checked by external testing + return fmt.Errorf("pre-authorization failed: %v", errors.Join(preAuthErrors...)) + } + return nil +} + +func getUserInfo(ext *ocv1.ClusterExtension) user.Info { + return &user.DefaultInfo{Name: fmt.Sprintf("system:serviceaccount:%s:%s", ext.Spec.Namespace, ext.Spec.ServiceAccount.Name)} +} + +func extManagementPerms(ext *ocv1.ClusterExtension) func(user.Info) []authorizer.AttributesRecord { + return func(user user.Info) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: user, + Name: ext.Name, + APIGroup: ocv1.GroupVersion.Group, + APIVersion: ocv1.GroupVersion.Version, + Resource: "clusterextensions/finalizers", + ResourceRequest: true, + Verb: "update", + }, + } + } +} diff --git a/internal/operator-controller/applier/helm_test.go b/internal/operator-controller/applier/helm_test.go index 8d07de9123..25fad4d66b 100644 --- a/internal/operator-controller/applier/helm_test.go +++ b/internal/operator-controller/applier/helm_test.go @@ -15,6 +15,9 @@ import ( "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" "sigs.k8s.io/controller-runtime/pkg/client" helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" @@ -70,16 +73,11 @@ type mockPreflight struct { } type mockPreAuthorizer struct { - missingRules []authorization.ScopedPolicyRules - returnError error + fn func(context.Context, user.Info, io.Reader, ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) } -func (p *mockPreAuthorizer) PreAuthorize( - ctx context.Context, - ext *ocv1.ClusterExtension, - manifestReader io.Reader, -) ([]authorization.ScopedPolicyRules, error) { - return p.missingRules, p.returnError +func (p *mockPreAuthorizer) PreAuthorize(ctx context.Context, manifestManager user.Info, manifestReader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return p.fn(ctx, manifestManager, manifestReader, additionalRequiredPerms...) } func (mp *mockPreflight) Install(context.Context, []client.Object) error { @@ -100,6 +98,7 @@ func (mockHelmReleaseToObjectsConverter) GetObjectsFromRelease(*release.Release) type mockActionGetter struct { actionClientForErr error getClientErr error + historyErr error installErr error dryRunInstallErr error upgradeErr error @@ -107,6 +106,7 @@ type mockActionGetter struct { reconcileErr error desiredRel *release.Release currentRel *release.Release + history []*release.Release } func (mag *mockActionGetter) ActionClientFor(ctx context.Context, obj client.Object) (helmclient.ActionInterface, error) { @@ -118,7 +118,7 @@ func (mag *mockActionGetter) Get(name string, opts ...helmclient.GetOption) (*re } func (mag *mockActionGetter) History(name string, opts ...helmclient.HistoryOption) ([]*release.Release, error) { - return nil, mag.getClientErr + return mag.history, mag.historyErr } func (mag *mockActionGetter) Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...helmclient.InstallOption) (*release.Release, error) { @@ -193,7 +193,17 @@ metadata: spec: clusterIP: 0.0.0.0` - testCE = &ocv1.ClusterExtension{} + testCE = &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + }, + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "test-namespace", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "test-sa", + }, + }, + } testObjectLabels = map[string]string{"object": "label"} testStorageLabels = map[string]string{"storage": "label"} errPreAuth = errors.New("problem running preauthorization") @@ -345,6 +355,52 @@ func TestApply_Installation(t *testing.T) { } func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { + t.Run("preauthorizer called with correct parameters", func(t *testing.T) { + mockAcg := &mockActionGetter{ + getClientErr: driver.ErrReleaseNotFound, + installErr: errors.New("failed installing chart"), + desiredRel: &release.Release{ + Info: &release.Info{Status: release.StatusDeployed}, + Manifest: validManifest, + }, + } + mockPf := &mockPreflight{installErr: errors.New("failed during install pre-flight check")} + helmApplier := applier.Helm{ + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + PreAuthorizer: &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + t.Log("has correct user") + require.Equal(t, "system:serviceaccount:test-namespace:test-sa", user.GetName()) + require.Empty(t, user.GetUID()) + require.Nil(t, user.GetExtra()) + require.Empty(t, user.GetGroups()) + + t.Log("has correct additional permissions") + require.Len(t, additionalRequiredPerms, 1) + perms := additionalRequiredPerms[0](user) + + require.Len(t, perms, 1) + require.Equal(t, authorizer.AttributesRecord{ + User: user, + Name: "test-ext", + APIGroup: "olm.operatorframework.io", + APIVersion: "v1", + Resource: "clusterextensions/finalizers", + ResourceRequest: true, + Verb: "update", + }, perms[0]) + return nil, nil + }, + }, + HelmChartProvider: DummyHelmChartProvider, + HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, + } + + _, _, err := helmApplier.Apply(context.TODO(), validFS, testCE, testObjectLabels, testStorageLabels) + require.Error(t, err) + }) + t.Run("fails during dry-run installation", func(t *testing.T) { mockAcg := &mockActionGetter{ getClientErr: driver.ErrReleaseNotFound, @@ -373,9 +429,13 @@ 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}, + ActionClientGetter: mockAcg, + Preflights: []applier.Preflight{mockPf}, + PreAuthorizer: &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return nil, nil + }, + }, HelmChartProvider: DummyHelmChartProvider, HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, } @@ -397,8 +457,12 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { } helmApplier := applier.Helm{ ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{nil, errPreAuth}, - HelmChartProvider: DummyHelmChartProvider, + PreAuthorizer: &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return nil, errPreAuth + }, + }, + HelmChartProvider: DummyHelmChartProvider, } // Use a ClusterExtension with valid Spec fields. validCE := &ocv1.ClusterExtension{ @@ -426,8 +490,12 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { } helmApplier := applier.Helm{ ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{missingRBAC, nil}, - HelmChartProvider: DummyHelmChartProvider, + PreAuthorizer: &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return missingRBAC, nil + }, + }, + HelmChartProvider: DummyHelmChartProvider, } // Use a ClusterExtension with valid Spec fields. validCE := &ocv1.ClusterExtension{ @@ -454,8 +522,12 @@ func TestApply_InstallationWithPreflightPermissionsEnabled(t *testing.T) { }, } helmApplier := applier.Helm{ - ActionClientGetter: mockAcg, - PreAuthorizer: &mockPreAuthorizer{nil, nil}, + ActionClientGetter: mockAcg, + PreAuthorizer: &mockPreAuthorizer{ + fn: func(ctx context.Context, user user.Info, reader io.Reader, additionalRequiredPerms ...authorization.UserAuthorizerAttributesFactory) ([]authorization.ScopedPolicyRules, error) { + return nil, nil + }, + }, HelmChartProvider: DummyHelmChartProvider, HelmReleaseToObjectsConverter: mockHelmReleaseToObjectsConverter{}, Manager: &mockManagedContentCacheManager{ diff --git a/internal/operator-controller/applier/phase.go b/internal/operator-controller/applier/phase.go index 6baa396cfc..5f6957506c 100644 --- a/internal/operator-controller/applier/phase.go +++ b/internal/operator-controller/applier/phase.go @@ -28,13 +28,14 @@ func determinePhase(gk schema.GroupKind) 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" + PhaseNamespaces Phase = "namespaces" + PhasePolicies Phase = "policies" + PhaseRBAC Phase = "rbac" + PhaseRBACBindings Phase = "rbac-bindings" + PhaseCRDs Phase = "crds" + PhaseStorage Phase = "storage" + PhaseDeploy Phase = "deploy" + PhasePublish Phase = "publish" ) // Well known phases ordered. @@ -42,6 +43,7 @@ var defaultPhaseOrder = []Phase{ PhaseNamespaces, PhasePolicies, PhaseRBAC, + PhaseRBACBindings, PhaseCRDs, PhaseStorage, PhaseDeploy, @@ -68,8 +70,11 @@ var ( PhaseRBAC: { {Kind: "ServiceAccount"}, {Kind: "Role", Group: "rbac.authorization.k8s.io"}, - {Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"}, {Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"}, + }, + + PhaseRBACBindings: { + {Kind: "RoleBinding", Group: "rbac.authorization.k8s.io"}, {Kind: "ClusterRoleBinding", Group: "rbac.authorization.k8s.io"}, }, diff --git a/internal/operator-controller/applier/phase_test.go b/internal/operator-controller/applier/phase_test.go index 6c0fe8fb32..7ca493648b 100644 --- a/internal/operator-controller/applier/phase_test.go +++ b/internal/operator-controller/applier/phase_test.go @@ -87,6 +87,30 @@ func Test_PhaseSort(t *testing.T) { }, }, }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + }, + }, + }, { Object: unstructured.Unstructured{ Object: map[string]interface{}{ @@ -150,6 +174,35 @@ func Test_PhaseSort(t *testing.T) { }, }, }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + }, + }, + }, + }, + }, + { + Name: string(applier.PhaseRBACBindings), + Objects: []v1.ClusterExtensionRevisionObject{ + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + }, + }, + }, + { + Object: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "RoleBinding", + }, + }, + }, }, }, { diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go index cf75b28c87..4602803ab6 100644 --- a/internal/operator-controller/applier/provider.go +++ b/internal/operator-controller/applier/provider.go @@ -14,8 +14,10 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/config" + "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" + errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error" ) // ManifestProvider returns the manifests that should be applied by OLM given a bundle and its associated ClusterExtension @@ -69,23 +71,44 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens } if r.IsSingleOwnNamespaceEnabled { - schema, err := rv1.GetConfigSchema() + configOpts, err := r.extractBundleConfigOptions(&rv1, ext) if err != nil { - return nil, fmt.Errorf("error getting configuration schema: %w", err) + return nil, err } + opts = append(opts, configOpts...) + } + return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) +} - bundleConfigBytes := extensionConfigBytes(ext) - bundleConfig, err := config.UnmarshalConfig(bundleConfigBytes, schema, ext.Spec.Namespace) - if err != nil { - return nil, fmt.Errorf("invalid ClusterExtension configuration: %w", err) - } +// extractBundleConfigOptions extracts and validates configuration options from a ClusterExtension. +// Returns render options for watchNamespace and deploymentConfig if present in the extension's configuration. +func (r *RegistryV1ManifestProvider) extractBundleConfigOptions(rv1 *bundle.RegistryV1, ext *ocv1.ClusterExtension) ([]render.Option, error) { + schema, err := rv1.GetConfigSchema() + if err != nil { + return nil, fmt.Errorf("error getting configuration schema: %w", err) + } - if watchNS := bundleConfig.GetWatchNamespace(); watchNS != nil { - opts = append(opts, render.WithTargetNamespaces(*watchNS)) + bundleConfigBytes := extensionConfigBytes(ext) + bundleConfig, err := config.UnmarshalConfig(bundleConfigBytes, schema, ext.Spec.Namespace) + if err != nil { + return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid ClusterExtension configuration: %w", err)) + } + + var opts []render.Option + if watchNS := bundleConfig.GetWatchNamespace(); watchNS != nil { + opts = append(opts, render.WithTargetNamespaces(*watchNS)) + } + + // Extract and convert deploymentConfig if present + if deploymentConfigMap := bundleConfig.GetDeploymentConfig(); deploymentConfigMap != nil { + deploymentConfig, err := convertToDeploymentConfig(deploymentConfigMap) + if err != nil { + return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid deploymentConfig: %w", err)) } + opts = append(opts, render.WithDeploymentConfig(deploymentConfig)) } - return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) + return opts, nil } // RegistryV1HelmChartProvider creates a Helm-Chart from a registry+v1 bundle and its associated ClusterExtension @@ -101,7 +124,7 @@ func (r *RegistryV1HelmChartProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExten 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 + // will go away once we have a bundle interface that can surface the annotations independently of the // underlying bundle format... rv1, err := source.FromFS(bundleFS).GetBundle() if err != nil { @@ -148,3 +171,37 @@ func extensionConfigBytes(ext *ocv1.ClusterExtension) []byte { } return nil } + +// convertToDeploymentConfig converts a map[string]any (from validated bundle config) +// to a *config.DeploymentConfig struct that can be passed to the renderer. +// Returns nil if the map is empty. +func convertToDeploymentConfig(deploymentConfigMap map[string]any) (*config.DeploymentConfig, error) { + if len(deploymentConfigMap) == 0 { + return nil, nil + } + + // Marshal the map to JSON + data, err := json.Marshal(deploymentConfigMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal deploymentConfig: %w", err) + } + + // Unmarshal into the DeploymentConfig struct + var deploymentConfig config.DeploymentConfig + if err := json.Unmarshal(data, &deploymentConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal deploymentConfig: %w", err) + } + + return &deploymentConfig, nil +} + +func getBundleAnnotations(bundleFS fs.FS) (map[string]string, error) { + // The need to get the underlying bundle in order to extract its annotations + // will go away once we 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 + } + return rv1.CSV.GetAnnotations(), nil +} diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go index 4138284a05..26d5652093 100644 --- a/internal/operator-controller/applier/provider_test.go +++ b/internal/operator-controller/applier/provider_test.go @@ -11,6 +11,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -100,6 +101,37 @@ func Test_RegistryV1ManifestProvider_Integration(t *testing.T) { require.Contains(t, err.Error(), "invalid ClusterExtension configuration") }) + t.Run("returns terminal error for invalid config", 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 + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + // Bundle with SingleNamespace install mode requiring watchNamespace config + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + // ClusterExtension without required config + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + // No config provided - should fail validation + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid ClusterExtension configuration") + // Assert that config validation errors are terminal (not retriable) + require.ErrorIs(t, err, reconcile.TerminalError(nil), "config validation errors should be terminal") + }) + t.Run("returns rendered manifests", func(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ BundleRenderer: registryv1.Renderer, @@ -393,7 +425,7 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { }) require.Error(t, err) require.Contains(t, err.Error(), "invalid ClusterExtension configuration:") - require.Contains(t, err.Error(), "watchNamespace must be") + require.Contains(t, err.Error(), "must be") require.Contains(t, err.Error(), "install-namespace") }) @@ -412,6 +444,218 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { }) } +func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) { + t.Run("passes deploymentConfig to renderer when provided in configuration", func(t *testing.T) { + expectedEnvVars := []corev1.EnvVar{ + {Name: "TEST_ENV", Value: "test-value"}, + } + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure deploymentConfig is passed to renderer") + require.NotNil(t, opts.DeploymentConfig) + require.Equal(t, expectedEnvVars, opts.DeploymentConfig.Env) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).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(`{"deploymentConfig": {"env": [{"name": "TEST_ENV", "value": "test-value"}]}}`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("does not pass deploymentConfig to renderer when not provided in configuration", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure deploymentConfig is nil when not provided") + require.Nil(t, opts.DeploymentConfig) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build() + + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + // No config provided + }, + }) + require.NoError(t, err) + }) + + t.Run("passes deploymentConfig with multiple fields to renderer", func(t *testing.T) { + expectedNodeSelector := map[string]string{"kubernetes.io/os": "linux"} + expectedTolerations := []corev1.Toleration{ + {Key: "key1", Operator: "Equal", Value: "value1", Effect: "NoSchedule"}, + } + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure all deploymentConfig fields are passed to renderer") + require.NotNil(t, opts.DeploymentConfig) + require.Equal(t, expectedNodeSelector, opts.DeploymentConfig.NodeSelector) + require.Equal(t, expectedTolerations, opts.DeploymentConfig.Tolerations) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).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(`{ + "deploymentConfig": { + "nodeSelector": {"kubernetes.io/os": "linux"}, + "tolerations": [{"key": "key1", "operator": "Equal", "value": "value1", "effect": "NoSchedule"}] + } + }`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("passes both watchNamespace and deploymentConfig when both provided", func(t *testing.T) { + expectedWatchNamespace := "some-namespace" + expectedEnvVars := []corev1.EnvVar{ + {Name: "TEST_ENV", Value: "test-value"}, + } + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure both watchNamespace and deploymentConfig are passed to renderer") + require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces) + require.NotNil(t, opts.DeploymentConfig) + require.Equal(t, expectedEnvVars, opts.DeploymentConfig.Env) + 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": "some-namespace", + "deploymentConfig": { + "env": [{"name": "TEST_ENV", "value": "test-value"}] + } + }`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("handles empty deploymentConfig gracefully", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure deploymentConfig is nil for empty config object") + require.Nil(t, opts.DeploymentConfig) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).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(`{"deploymentConfig": {}}`), + }, + }, + }, + }) + require.NoError(t, err) + }) + + t.Run("returns terminal error when deploymentConfig has invalid structure", 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 + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build() + + // Provide deploymentConfig with invalid structure - env should be array, not string + // Schema validation catches this before conversion + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"deploymentConfig": {"env": "not-an-array"}}`), + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid ClusterExtension configuration") + require.Contains(t, err.Error(), "deploymentConfig.env") + require.ErrorIs(t, err, reconcile.TerminalError(nil), "config validation errors should be terminal") + }) +} + func Test_RegistryV1HelmChartProvider_Integration(t *testing.T) { t.Run("surfaces bundle source errors", func(t *testing.T) { provider := applier.RegistryV1HelmChartProvider{ diff --git a/internal/operator-controller/authorization/rbac.go b/internal/operator-controller/authorization/rbac.go index 357268615c..d85e532f8b 100644 --- a/internal/operator-controller/authorization/rbac.go +++ b/internal/operator-controller/authorization/rbac.go @@ -31,12 +31,25 @@ import ( "k8s.io/kubernetes/pkg/registry/rbac/validation" rbac "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac" "sigs.k8s.io/controller-runtime/pkg/client" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" ) +// UserAuthorizerAttributesFactory is a function that produces a slice of AttributesRecord for user +type UserAuthorizerAttributesFactory func(user user.Info) []authorizer.AttributesRecord + type PreAuthorizer interface { - PreAuthorize(ctx context.Context, ext *ocv1.ClusterExtension, manifestReader io.Reader) ([]ScopedPolicyRules, error) + // PreAuthorize validates whether the user satisfies the necessary permissions + // as defined by the RBAC policy. It examines the user’s roles, resource identifiers, and + // the intended action to determine if the operation is allowed. Optional additional required permissions are also evaluated + // against user. + // + // Return Value: + // - nil: indicates that the authorization check passed and the operation is permitted. + // - non-nil error: indicates that an error occurred during the permission evaluation process + // (for example, a failure decoding the manifest or other internal issues). If the evaluation + // completes successfully but identifies missing rules, then a nil error is returned along with + // the list (or slice) of missing rules. Note that in some cases the error may encapsulate multiple + // evaluation failures + PreAuthorize(ctx context.Context, user user.Info, manifestReader io.Reader, additionalRequiredPerms ...UserAuthorizerAttributesFactory) ([]ScopedPolicyRules, error) } type ScopedPolicyRules struct { @@ -68,24 +81,19 @@ func NewRBACPreAuthorizer(cl client.Client) PreAuthorizer { } } -// PreAuthorize validates whether the current user/request satisfies the necessary permissions -// as defined by the RBAC policy. It examines the user’s roles, resource identifiers, and -// the intended action to determine if the operation is allowed. -// -// Return Value: -// - nil: indicates that the authorization check passed and the operation is permitted. -// - non-nil error: indicates that an error occurred during the permission evaluation process -// (for example, a failure decoding the manifest or other internal issues). If the evaluation -// completes successfully but identifies missing rules, then a nil error is returned along with -// the list (or slice) of missing rules. Note that in some cases the error may encapsulate multiple -// evaluation failures -func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, ext *ocv1.ClusterExtension, manifestReader io.Reader) ([]ScopedPolicyRules, error) { +func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, user user.Info, manifestReader io.Reader, additionalRequiredPerms ...UserAuthorizerAttributesFactory) ([]ScopedPolicyRules, error) { dm, err := a.decodeManifest(manifestReader) if err != nil { return nil, err } - manifestManager := &user.DefaultInfo{Name: fmt.Sprintf("system:serviceaccount:%s:%s", ext.Spec.Namespace, ext.Spec.ServiceAccount.Name)} - attributesRecords := dm.asAuthorizationAttributesRecordsForUser(manifestManager, ext) + + // derive manifest related attributes records + attributesRecords := dm.asAuthorizationAttributesRecordsForUser(user) + + // append additional required perms + for _, fn := range additionalRequiredPerms { + attributesRecords = append(attributesRecords, fn(user)...) + } var preAuthEvaluationErrors []error missingRules, err := a.authorizeAttributesRecords(ctx, attributesRecords) @@ -97,7 +105,7 @@ func (a *rbacPreAuthorizer) PreAuthorize(ctx context.Context, ext *ocv1.ClusterE var parseErrors []error for _, obj := range dm.rbacObjects() { - if err := ec.checkEscalation(ctx, manifestManager, obj); err != nil { + if err := ec.checkEscalation(ctx, user, obj); err != nil { result, err := parseEscalationErrorForMissingRules(err) missingRules[obj.GetNamespace()] = append(missingRules[obj.GetNamespace()], result.MissingRules...) preAuthEvaluationErrors = append(preAuthEvaluationErrors, result.ResolutionErrors) @@ -316,8 +324,18 @@ func (dm *decodedManifest) rbacObjects() []client.Object { return objects } -func (dm *decodedManifest) asAuthorizationAttributesRecordsForUser(manifestManager user.Info, ext *ocv1.ClusterExtension) []authorizer.AttributesRecord { - var attributeRecords []authorizer.AttributesRecord +func (dm *decodedManifest) asAuthorizationAttributesRecordsForUser(manifestManager user.Info) []authorizer.AttributesRecord { + // Calculate initial capacity as an upper-bound estimate: + // - For each key: len(objectVerbs) records (4) + // - For unique namespaces: len(namespacedCollectionVerbs) records (1 per unique namespace across all keys in a GVR) + // We use totalKeys as upper bound (worst case: each key in different namespace) + // - For each GVR: len(clusterCollectionVerbs) records (2) + totalKeys := 0 + for _, keys := range dm.gvrs { + totalKeys += len(keys) + } + estimatedCapacity := totalKeys*len(objectVerbs) + totalKeys*len(namespacedCollectionVerbs) + len(dm.gvrs)*len(clusterCollectionVerbs) + attributeRecords := make([]authorizer.AttributesRecord, 0, estimatedCapacity) for gvr, keys := range dm.gvrs { namespaces := sets.New[string]() @@ -363,18 +381,6 @@ func (dm *decodedManifest) asAuthorizationAttributesRecordsForUser(manifestManag Verb: v, }) } - - for _, verb := range []string{"update"} { - attributeRecords = append(attributeRecords, authorizer.AttributesRecord{ - User: manifestManager, - Name: ext.Name, - APIGroup: ext.GroupVersionKind().Group, - APIVersion: ext.GroupVersionKind().Version, - Resource: "clusterextensions/finalizers", - ResourceRequest: true, - Verb: verb, - }) - } } return attributeRecords } diff --git a/internal/operator-controller/authorization/rbac_test.go b/internal/operator-controller/authorization/rbac_test.go index 8e3d47687d..9d80cc52bb 100644 --- a/internal/operator-controller/authorization/rbac_test.go +++ b/internal/operator-controller/authorization/rbac_test.go @@ -14,12 +14,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/kubernetes/pkg/registry/rbac/validation" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" ) var ( @@ -129,17 +128,9 @@ subjects: namespace: a-test-namespace ` - saName = "test-serviceaccount" - ns = "test-namespace" - exampleClusterExtension = ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test-cluster-extension"}, - Spec: ocv1.ClusterExtensionSpec{ - Namespace: ns, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: saName, - }, - }, - } + saName = "test-serviceaccount" + ns = "test-namespace" + testUser = &user.DefaultInfo{Name: fmt.Sprintf("system:serviceaccount:%s:%s", ns, saName)} objects = []client.Object{ &corev1.Namespace{ @@ -204,7 +195,7 @@ subjects: Rules: []rbacv1.PolicyRule{ { APIGroups: []string{"*"}, - Resources: []string{"serviceaccounts", "services", "clusterextensions/finalizers"}, + Resources: []string{"serviceaccounts", "services"}, Verbs: []string{"*"}, }, { @@ -236,12 +227,6 @@ subjects: APIGroups: []string{"rbac.authorization.k8s.io"}, Resources: []string{"roles"}, ResourceNames: []string(nil), - NonResourceURLs: []string(nil)}, - { - Verbs: []string{"update"}, - APIGroups: []string{""}, - Resources: []string{"clusterextensions/finalizers"}, - ResourceNames: []string{"test-cluster-extension"}, NonResourceURLs: []string(nil), }, }, @@ -310,12 +295,6 @@ subjects: APIGroups: []string{"rbac.authorization.k8s.io"}, Resources: []string{"roles"}, ResourceNames: []string(nil), - NonResourceURLs: []string(nil)}, - { - Verbs: []string{"update"}, - APIGroups: []string{""}, - Resources: []string{"clusterextensions/finalizers"}, - ResourceNames: []string{"test-cluster-extension"}, NonResourceURLs: []string(nil), }, }, @@ -418,7 +397,7 @@ func TestPreAuthorize_Success(t *testing.T) { t.Run("preauthorize succeeds with no missing rbac rules", func(t *testing.T) { fakeClient := setupFakeClient(privilegedClusterRole) preAuth := NewRBACPreAuthorizer(fakeClient) - missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest)) + missingRules, err := preAuth.PreAuthorize(context.TODO(), testUser, strings.NewReader(testManifest)) require.NoError(t, err) require.Equal(t, []ScopedPolicyRules{}, missingRules) }) @@ -428,7 +407,7 @@ func TestPreAuthorize_MissingRBAC(t *testing.T) { t.Run("preauthorize fails and finds missing rbac rules", func(t *testing.T) { fakeClient := setupFakeClient(limitedClusterRole) preAuth := NewRBACPreAuthorizer(fakeClient) - missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest)) + missingRules, err := preAuth.PreAuthorize(context.TODO(), testUser, strings.NewReader(testManifest)) require.NoError(t, err) require.Equal(t, expectedSingleNamespaceMissingRules, missingRules) }) @@ -438,7 +417,7 @@ func TestPreAuthorizeMultiNamespace_MissingRBAC(t *testing.T) { t.Run("preauthorize fails and finds missing rbac rules in multiple namespaces", func(t *testing.T) { fakeClient := setupFakeClient(limitedClusterRole) preAuth := NewRBACPreAuthorizer(fakeClient) - missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifestMultiNamespace)) + missingRules, err := preAuth.PreAuthorize(context.TODO(), testUser, strings.NewReader(testManifestMultiNamespace)) require.NoError(t, err) require.Equal(t, expectedMultiNamespaceMissingRules, missingRules) }) @@ -448,12 +427,44 @@ func TestPreAuthorize_CheckEscalation(t *testing.T) { t.Run("preauthorize succeeds with no missing rbac rules", func(t *testing.T) { fakeClient := setupFakeClient(escalatingClusterRole) preAuth := NewRBACPreAuthorizer(fakeClient) - missingRules, err := preAuth.PreAuthorize(context.TODO(), &exampleClusterExtension, strings.NewReader(testManifest)) + missingRules, err := preAuth.PreAuthorize(context.TODO(), testUser, strings.NewReader(testManifest)) require.NoError(t, err) require.Equal(t, []ScopedPolicyRules{}, missingRules) }) } +func TestPreAuthorize_AdditionalRequiredPerms_MissingRBAC(t *testing.T) { + t.Run("preauthorize fails and finds missing rbac rules coming from the additional required permissions", func(t *testing.T) { + fakeClient := setupFakeClient(escalatingClusterRole) + preAuth := NewRBACPreAuthorizer(fakeClient) + missingRules, err := preAuth.PreAuthorize(context.TODO(), testUser, strings.NewReader(testManifest), func(user user.Info) []authorizer.AttributesRecord { + return []authorizer.AttributesRecord{ + { + User: user, + Verb: "create", + APIGroup: corev1.SchemeGroupVersion.Group, + APIVersion: corev1.SchemeGroupVersion.Version, + Resource: "pods", + ResourceRequest: true, + }, + } + }) + require.NoError(t, err) + require.Equal(t, []ScopedPolicyRules{ + { + Namespace: "", + MissingRules: []rbacv1.PolicyRule{ + { + Verbs: []string{"create"}, + APIGroups: []string{""}, + Resources: []string{"pods"}, + }, + }, + }, + }, missingRules) + }) +} + // TestParseEscalationErrorForMissingRules Are tests with respect to https://github.com/kubernetes/api/blob/e8d4d542f6a9a16a694bfc8e3b8cd1557eecfc9d/rbac/v1/types.go#L49-L74 // Goal is: prove the regex works as planned AND that if the error messages ever change we'll learn about it with these tests func TestParseEscalationErrorForMissingRules_ParsingLogic(t *testing.T) { diff --git a/internal/operator-controller/conditionsets/conditionsets.go b/internal/operator-controller/conditionsets/conditionsets.go index 6c33b1c8f9..97073a02d8 100644 --- a/internal/operator-controller/conditionsets/conditionsets.go +++ b/internal/operator-controller/conditionsets/conditionsets.go @@ -36,9 +36,13 @@ var ConditionTypes = []string{ var ConditionReasons = []string{ ocv1.ReasonSucceeded, ocv1.ReasonDeprecated, + ocv1.ReasonNotDeprecated, + ocv1.ReasonDeprecationStatusUnknown, ocv1.ReasonFailed, ocv1.ReasonBlocked, + ocv1.ReasonInvalidConfiguration, ocv1.ReasonRetrying, ocv1.ReasonAbsent, ocv1.ReasonRollingOut, + ocv1.ReasonProgressDeadlineExceeded, } diff --git a/internal/operator-controller/config/config.go b/internal/operator-controller/config/config.go index 8fcadf40ad..afb89dff58 100644 --- a/internal/operator-controller/config/config.go +++ b/internal/operator-controller/config/config.go @@ -31,7 +31,10 @@ import ( "strings" "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/santhosh-tekuri/jsonschema/v6/kind" "sigs.k8s.io/yaml" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" ) const ( @@ -47,6 +50,10 @@ const ( FormatSingleNamespaceInstallMode = "singleNamespaceInstallMode" ) +// DeploymentConfig is a type alias for v1alpha1.SubscriptionConfig +// to maintain clear naming in the OLMv1 context while reusing the v0 type. +type DeploymentConfig = v1alpha1.SubscriptionConfig + // SchemaProvider lets each package format type describe what configuration it accepts. // // Different package format types provide schemas in different ways: @@ -98,6 +105,47 @@ func (c *Config) GetWatchNamespace() *string { return &str } +// GetDeploymentConfig returns the deploymentConfig value if present in the configuration. +// Returns nil if deploymentConfig is not set or is explicitly set to null. +// The returned value is a generic map[string]any that can be marshaled to JSON +// for validation or conversion to specific types (like v1alpha1.SubscriptionConfig). +// +// Returns a defensive deep copy so callers can't mutate the internal Config state. +func (c *Config) GetDeploymentConfig() map[string]any { + if c == nil || *c == nil { + return nil + } + val, exists := (*c)["deploymentConfig"] + if !exists { + return nil + } + // User set deploymentConfig: null - treat as "not configured" + if val == nil { + return nil + } + // Schema validation ensures this is an object (map) + dcMap, ok := val.(map[string]any) + if !ok { + return nil + } + + // Return a defensive deep copy so callers can't mutate the internal Config state. + // We use JSON marshal/unmarshal because the data is already JSON-compatible and + // this handles nested structures correctly. + data, err := json.Marshal(dcMap) + if err != nil { + // This should never happen since the map came from validated JSON/YAML, + // but return nil as a safe fallback + return nil + } + var copied map[string]any + if err := json.Unmarshal(data, &copied); err != nil { + // This should never happen for valid JSON + return nil + } + return copied +} + // UnmarshalConfig takes user configuration, validates it, and creates a Config object. // This is the only way to create a Config. // @@ -167,7 +215,7 @@ func validateConfigWithSchema(configBytes []byte, schema map[string]any, install return fmt.Errorf("value must be a string") } if str != installNamespace { - return fmt.Errorf("invalid value %q: watchNamespace must be %q (the namespace where the operator is installed) because this operator only supports OwnNamespace install mode", str, installNamespace) + return fmt.Errorf("invalid value %q: must be %q (the namespace where the operator is installed) because this operator only supports OwnNamespace install mode", str, installNamespace) } return nil }, @@ -187,7 +235,7 @@ func validateConfigWithSchema(configBytes []byte, schema map[string]any, install return fmt.Errorf("value must be a string") } if str == installNamespace { - return fmt.Errorf("invalid value %q: watchNamespace must be different from %q (the install namespace) because this operator uses SingleNamespace install mode to watch a different namespace", str, installNamespace) + return fmt.Errorf("invalid value %q: must be different from %q (the install namespace) because this operator uses SingleNamespace install mode to watch a different namespace", str, installNamespace) } return nil }, @@ -253,9 +301,13 @@ func formatSchemaError(err error) error { // formatSingleError formats a single validation error from the schema library. func formatSingleError(errUnit jsonschema.OutputUnit) string { + if errUnit.Error == nil { + return "" + } + // Check the keyword location to identify the error type - switch { - case strings.Contains(errUnit.KeywordLocation, "/required"): + switch errKind := errUnit.Error.Kind.(type) { + case *kind.Required: // Missing required field fieldName := extractFieldNameFromMessage(errUnit.Error) if fieldName != "" { @@ -263,7 +315,7 @@ func formatSingleError(errUnit jsonschema.OutputUnit) string { } return "required field is missing" - case strings.Contains(errUnit.KeywordLocation, "/additionalProperties"): + case *kind.AdditionalProperties: // Unknown/additional field fieldName := extractFieldNameFromMessage(errUnit.Error) if fieldName != "" { @@ -271,7 +323,7 @@ func formatSingleError(errUnit jsonschema.OutputUnit) string { } return "unknown field" - case strings.Contains(errUnit.KeywordLocation, "/type"): + case *kind.Type: // Type mismatch (e.g., got null, want string) fieldPath := buildFieldPath(errUnit.InstanceLocation) if fieldPath != "" { @@ -283,16 +335,14 @@ func formatSingleError(errUnit jsonschema.OutputUnit) string { } return fmt.Sprintf("invalid type: %s", errUnit.Error.String()) - case strings.Contains(errUnit.KeywordLocation, "/format"): - // Custom format validation (e.g., OwnNamespace, SingleNamespace constraints) - // These already have good error messages from our custom validators - if errUnit.Error != nil { - return errUnit.Error.String() - } + case *kind.Format: fieldPath := buildFieldPath(errUnit.InstanceLocation) - return fmt.Sprintf("invalid format for field %q", fieldPath) + if fieldPath != "" { + return fmt.Sprintf("invalid format for field %q: %s", fieldPath, errUnit.Error.String()) + } + return fmt.Sprintf("invalid format: %s", errUnit.Error.String()) - case strings.Contains(errUnit.KeywordLocation, "/anyOf"): + case *kind.AnyOf: // anyOf validation failed - could be null or wrong type // This happens when a field accepts [null, string] but got something else fieldPath := buildFieldPath(errUnit.InstanceLocation) @@ -301,13 +351,31 @@ func formatSingleError(errUnit jsonschema.OutputUnit) string { } return "invalid value" + case *kind.MaxLength: + fieldPath := buildFieldPath(errUnit.InstanceLocation) + if fieldPath != "" { + return fmt.Sprintf("field %q must have maximum length of %d (len=%d)", fieldPath, errKind.Want, errKind.Got) + } + return errUnit.Error.String() + + case *kind.MinLength: + fieldPath := buildFieldPath(errUnit.InstanceLocation) + if fieldPath != "" { + return fmt.Sprintf("field %q must have minimum length of %d (len=%d)", fieldPath, errKind.Want, errKind.Got) + } + return errUnit.Error.String() + + case *kind.Pattern: + fieldPath := buildFieldPath(errUnit.InstanceLocation) + if fieldPath != "" { + return fmt.Sprintf("field %q must match pattern %q", fieldPath, errKind.Want) + } + return errUnit.Error.String() + default: - // Unknown error type - return the library's error message + // Unhandled error type - return the library's error message // This serves as a fallback for future schema features we haven't customized yet - if errUnit.Error != nil { - return errUnit.Error.String() - } - return "" + return errUnit.Error.String() } } diff --git a/internal/operator-controller/config/config_test.go b/internal/operator-controller/config/config_test.go index 95bb98f0b4..d49bc6b877 100644 --- a/internal/operator-controller/config/config_test.go +++ b/internal/operator-controller/config/config_test.go @@ -337,8 +337,8 @@ func Test_UnmarshalConfig_EmptySchema(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - emptySchemaBundle := &mockEmptySchemaBundle{} - schema, err := emptySchemaBundle.GetConfigSchema() + noSchemaBundle := &mockNoSchemaBundle{} + schema, err := noSchemaBundle.GetConfigSchema() require.NoError(t, err) config, err := config.UnmarshalConfig(tc.rawConfig, schema, "my-namespace") @@ -566,9 +566,135 @@ func (h *mockHelmBundle) GetConfigSchema() (map[string]any, error) { return schemaMap, nil } -// mockEmptySchemaBundle represents a ClusterExtension that doesn't provide a configuration schema. -type mockEmptySchemaBundle struct{} +// mockNoSchemaBundle represents a bundle that doesn't provide a configuration schema. +type mockNoSchemaBundle struct{} -func (e *mockEmptySchemaBundle) GetConfigSchema() (map[string]any, error) { +func (e *mockNoSchemaBundle) GetConfigSchema() (map[string]any, error) { + // Return nil to indicate "no schema" (skip validation) return nil, nil } + +// Test_GetDeploymentConfig tests the GetDeploymentConfig accessor method. +func Test_GetDeploymentConfig(t *testing.T) { + // Create a bundle that returns nil schema (no validation) + bundle := &mockNoSchemaBundle{} + + tests := []struct { + name string + rawConfig []byte + expectedDeploymentConfig map[string]any + expectedDeploymentConfigNil bool + }{ + { + name: "empty config returns nil", + rawConfig: []byte(`{}`), + expectedDeploymentConfigNil: true, + }, + { + name: "config without deploymentConfig field returns nil", + rawConfig: []byte(`{"watchNamespace": "test-ns"}`), + expectedDeploymentConfigNil: true, + }, + { + name: "config with null deploymentConfig returns nil", + rawConfig: []byte(`{"deploymentConfig": null}`), + expectedDeploymentConfigNil: true, + }, + { + name: "config with valid deploymentConfig returns the object", + rawConfig: []byte(`{ + "deploymentConfig": { + "nodeSelector": { + "kubernetes.io/os": "linux" + }, + "resources": { + "requests": { + "memory": "128Mi" + } + } + } + }`), + expectedDeploymentConfig: map[string]any{ + "nodeSelector": map[string]any{ + "kubernetes.io/os": "linux", + }, + "resources": map[string]any{ + "requests": map[string]any{ + "memory": "128Mi", + }, + }, + }, + expectedDeploymentConfigNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + schema, err := bundle.GetConfigSchema() + require.NoError(t, err) + + cfg, err := config.UnmarshalConfig(tt.rawConfig, schema, "") + require.NoError(t, err) + + result := cfg.GetDeploymentConfig() + if tt.expectedDeploymentConfigNil { + require.Nil(t, result) + } else { + require.NotNil(t, result) + require.Equal(t, tt.expectedDeploymentConfig, result) + } + }) + } + + // Test nil config separately + t.Run("nil config returns nil", func(t *testing.T) { + var cfg *config.Config + result := cfg.GetDeploymentConfig() + require.Nil(t, result) + }) + + // Test that returned map is a defensive copy (mutations don't affect original) + t.Run("returned map is defensive copy - mutations don't affect original", func(t *testing.T) { + rawConfig := []byte(`{ + "deploymentConfig": { + "nodeSelector": { + "kubernetes.io/os": "linux" + } + } + }`) + + schema, err := bundle.GetConfigSchema() + require.NoError(t, err) + + cfg, err := config.UnmarshalConfig(rawConfig, schema, "") + require.NoError(t, err) + + // Get the deploymentConfig + result1 := cfg.GetDeploymentConfig() + require.NotNil(t, result1) + + // Mutate the returned map + result1["nodeSelector"] = map[string]any{ + "mutated": "value", + } + result1["newField"] = "added" + + // Get deploymentConfig again - should be unaffected by mutations + result2 := cfg.GetDeploymentConfig() + require.NotNil(t, result2) + + // Original values should be intact + require.Equal(t, map[string]any{ + "nodeSelector": map[string]any{ + "kubernetes.io/os": "linux", + }, + }, result2) + + // New field should not exist + _, exists := result2["newField"] + require.False(t, exists) + + // result1 should have the mutations + require.Equal(t, "added", result1["newField"]) + }) +} diff --git a/internal/operator-controller/config/error_formatting_test.go b/internal/operator-controller/config/error_formatting_test.go index 557e21019b..6ec1ebf7de 100644 --- a/internal/operator-controller/config/error_formatting_test.go +++ b/internal/operator-controller/config/error_formatting_test.go @@ -1,6 +1,7 @@ package config_test import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -72,9 +73,9 @@ func Test_ErrorFormatting_SchemaLibraryVersion(t *testing.T) { supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, installNamespace: "correct-namespace", expectedErrSubstrings: []string{ + "invalid format for field \"watchNamespace\"", "invalid value", "wrong-namespace", - "watchNamespace must be", "correct-namespace", "the namespace where the operator is installed", "OwnNamespace install mode", @@ -86,14 +87,38 @@ func Test_ErrorFormatting_SchemaLibraryVersion(t *testing.T) { supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, installNamespace: "install-ns", expectedErrSubstrings: []string{ + "invalid format for field \"watchNamespace\"", + "not valid singleNamespaceInstallMode", "invalid value", "install-ns", - "watchNamespace must be different from", + "must be different from", "the install namespace", "SingleNamespace install mode", "watch a different namespace", }, }, + { + name: "SingleNamespace constraint error bad namespace format", + rawConfig: []byte(`{"watchNamespace": "---AAAA-BBBB-super-long-namespace-that-that-is-waaaaaaaaayyy-longer-than-sixty-three-characters"}`), + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + installNamespace: "install-ns", + expectedErrSubstrings: []string{ + "field \"watchNamespace\"", + "must match pattern \"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\"", + }, + }, + { + name: "Single- and OwnNamespace constraint error bad namespace format", + rawConfig: []byte(`{"watchNamespace": ` + strings.Repeat("A", 63) + `"}`), + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + installNamespace: "install-ns", + expectedErrSubstrings: []string{ + "invalid configuration", + "multiple errors found", + "must have maximum length of 63 (len=64)", + "must match pattern \"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$\"", + }, + }, } { t.Run(tc.name, func(t *testing.T) { rv1 := bundle.RegistryV1{ diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go index 01bb2232d6..dfb5dad145 100644 --- a/internal/operator-controller/controllers/boxcutter_reconcile_steps.go +++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps.go @@ -19,13 +19,16 @@ package controllers import ( "cmp" "context" + "errors" "fmt" + "io/fs" "slices" apimeta "k8s.io/apimachinery/pkg/api/meta" 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/reconcile" ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" @@ -93,7 +96,7 @@ func MigrateStorage(m StorageMigrator) ReconcileStepFunc { } } -func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc { +func ApplyBundleWithBoxcutter(apply func(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error)) ReconcileStepFunc { return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { l := log.FromContext(ctx) revisionAnnotations := map[string]string{ @@ -108,13 +111,21 @@ func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc { } l.Info("applying bundle contents") - if _, _, err := a.Apply(ctx, state.imageFS, ext, objLbls, revisionAnnotations); err != nil { + _, _, err := apply(ctx, state.imageFS, ext, objLbls, revisionAnnotations) + if err != nil { // If there was an error applying the resolved bundle, // report the error via the Progressing condition. setStatusProgressing(ext, wrapErrorWithResolutionInfo(state.resolvedRevisionMetadata.BundleMetadata, err)) + // Only set Installed condition for retryable errors. + // For terminal errors (Progressing: False with a terminal reason such as Blocked or InvalidConfiguration), + // the Progressing condition already provides all necessary information about the failure. + if !errors.Is(err, reconcile.TerminalError(nil)) { + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + } return nil, err } + ext.Status.ActiveRevisions = []ocv1.RevisionStatus{} // Mirror Available/Progressing conditions from the installed revision if i := state.revisionStates.Installed; i != nil { for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} { diff --git a/internal/operator-controller/controllers/boxcutter_reconcile_steps_apply_test.go b/internal/operator-controller/controllers/boxcutter_reconcile_steps_apply_test.go new file mode 100644 index 0000000000..78adb70090 --- /dev/null +++ b/internal/operator-controller/controllers/boxcutter_reconcile_steps_apply_test.go @@ -0,0 +1,150 @@ +/* +Copyright 2026. + +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" + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" +) + +func TestApplyBundleWithBoxcutter(t *testing.T) { + type args struct { + activeRevisions []ocv1.RevisionStatus + revisionStates *RevisionStates + } + type want struct { + activeRevisions []ocv1.RevisionStatus + } + + for _, tc := range []struct { + name string + args args + want want + }{ + { + name: "two active revisions during update", + args: args{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-1"}, + }, + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "ce-1", + BundleMetadata: ocv1.BundleMetadata{ + Name: "test-bundle", + Version: "1.0.0", + }, + }, + RollingOut: []*RevisionMetadata{ + {RevisionName: "ce-2"}, + }, + }, + }, + want: want{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-1"}, + {Name: "ce-2"}, + }, + }, + }, + { + name: "replaces existing with new active revisions", + args: args{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-1"}, + }, + revisionStates: &RevisionStates{ + Installed: &RevisionMetadata{ + RevisionName: "ce-2", + BundleMetadata: ocv1.BundleMetadata{ + Name: "test-bundle", + Version: "1.0.1", + }, + }, + }, + }, + want: want{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-2"}, + }, + }, + }, + { + name: "ongoing installation", + args: args{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-1"}, + }, + revisionStates: &RevisionStates{ + RollingOut: []*RevisionMetadata{ + {RevisionName: "ce-1"}, + }, + }, + }, + want: want{ + activeRevisions: []ocv1.RevisionStatus{ + {Name: "ce-1"}, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + ActiveRevisions: tc.args.activeRevisions, + }, + } + + state := &reconcileState{ + revisionStates: tc.args.revisionStates, + resolvedRevisionMetadata: &RevisionMetadata{ + BundleMetadata: ocv1.BundleMetadata{ + Name: "test-bundle", + Version: "1.0.0", + }, + }, + imageFS: fstest.MapFS{}, + } + + stepFunc := ApplyBundleWithBoxcutter(func(_ context.Context, _ fs.FS, _ *ocv1.ClusterExtension, _, _ map[string]string) (bool, string, error) { + return true, "", nil + }) + result, err := stepFunc(ctx, state, ext) + require.NoError(t, err) + require.Nil(t, result) + + require.Len(t, ext.Status.ActiveRevisions, len(tc.want.activeRevisions)) + for i, expected := range tc.want.activeRevisions { + require.Equal(t, expected.Name, ext.Status.ActiveRevisions[i].Name, + "ActiveRevisions[%d].Name mismatch", i) + } + }) + } +} diff --git a/internal/operator-controller/controllers/clusterextension_admission_test.go b/internal/operator-controller/controllers/clusterextension_admission_test.go index 2f8791999f..259bb95059 100644 --- a/internal/operator-controller/controllers/clusterextension_admission_test.go +++ b/internal/operator-controller/controllers/clusterextension_admission_test.go @@ -13,7 +13,7 @@ import ( ) func TestClusterExtensionSourceConfig(t *testing.T) { - sourceTypeEmptyError := "Invalid value: null" + sourceTypeEmptyErrors := []string{"Invalid value: \"null\"", "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 @@ -21,12 +21,12 @@ func TestClusterExtensionSourceConfig(t *testing.T) { name string sourceType string unionField string - errMsg string + errMsgs []string }{ - {"sourceType is null", "", "Catalog", sourceTypeEmptyError}, - {"sourceType is invalid", "Invalid", "Catalog", sourceTypeMismatchError}, - {"catalog field does not exist", "Catalog", "", sourceConfigInvalidError}, - {"sourceConfig has required fields", "Catalog", "Catalog", ""}, + {"sourceType is null", "", "Catalog", sourceTypeEmptyErrors}, + {"sourceType is invalid", "Invalid", "Catalog", []string{sourceTypeMismatchError}}, + {"catalog field does not exist", "Catalog", "", []string{sourceConfigInvalidError}}, + {"sourceConfig has required fields", "Catalog", "Catalog", []string{}}, } t.Parallel() @@ -62,12 +62,20 @@ func TestClusterExtensionSourceConfig(t *testing.T) { })) } - if tc.errMsg == "" { + if len(tc.errMsgs) == 0 { require.NoError(t, err, "unexpected error for sourceType %q: %w", tc.sourceType, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) + return + } + + require.Error(t, err) + matched := false + for _, msg := range tc.errMsgs { + if strings.Contains(err.Error(), msg) { + matched = true + break + } } + require.True(t, matched, "expected one of %v in error %q", tc.errMsgs, err) }) } } diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 7f3192b0fd..a3fcf48f3f 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io/fs" + "slices" "strings" "github.com/go-logr/logr" @@ -49,6 +50,7 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" + errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error" k8sutil "github.com/operator-framework/operator-controller/internal/shared/util/k8s" ) @@ -166,13 +168,22 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req return res, reconcileErr } -// ensureAllConditionsWithReason checks that all defined condition types exist in the given ClusterExtension, -// and assigns a specified reason and custom message to any missing condition. -func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.ConditionReason, message string) { +// ensureFailureConditionsWithReason keeps every non-deprecation condition present. +// If one is missing, we add it with the given reason and message so users see why +// reconcile failed. Deprecation conditions are handled later by SetDeprecationStatus. +// +//nolint:unparam // reason parameter is designed to be flexible, even if current callers use the same value +func ensureFailureConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.ConditionReason, message string) { for _, condType := range conditionsets.ConditionTypes { + if isDeprecationCondition(condType) { + continue + } cond := apimeta.FindStatusCondition(ext.Status.Conditions, condType) + // Guard so we only fill empty slots. Without it, we would overwrite the detailed status that + // helpers (setStatusProgressing, setInstalledStatusCondition*, SetDeprecationStatus) already set. if cond == nil { - // Create a new condition with a valid reason and add it + // No condition exists yet, so add a fallback with the failure reason. Specific helpers replace it + // with the real progressing/bundle/package/channel message during reconciliation. SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ Type: condType, Status: metav1.ConditionFalse, @@ -184,83 +195,224 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C } } -// SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension -// based on the provided bundle -func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, deprecation *declcfg.Deprecation) { - deprecations := map[string][]declcfg.DeprecationEntry{} +// SetDeprecationStatus updates deprecation conditions based on catalog metadata. +// +// Behavior (following Kubernetes API conventions - conditions always present): +// - IS deprecated -> condition True with Reason: Deprecated +// - NOT deprecated -> condition False with Reason: NotDeprecated +// - Can't check (no catalog) -> condition Unknown with Reason: DeprecationStatusUnknown +// - No bundle installed -> BundleDeprecated Unknown with Reason: Absent +// +// This keeps deprecation conditions focused on catalog data. Install/validation errors +// never appear here - they belong in Progressing/Installed conditions. +func SetDeprecationStatus(ext *ocv1.ClusterExtension, installedBundleName string, deprecation *declcfg.Deprecation, hasCatalogData bool) { + info := buildDeprecationInfo(ext, installedBundleName, deprecation) + packageMessages := collectDeprecationMessages(info.PackageEntries) + channelMessages := collectDeprecationMessages(info.ChannelEntries) + bundleMessages := collectDeprecationMessages(info.BundleEntries) + + // Strategy: Always set deprecation conditions (following Kubernetes API conventions). + // SetStatusCondition preserves lastTransitionTime when status/reason/message haven't changed, + // preventing infinite reconciliation loops. + // - True = deprecated + // - False = not deprecated (verified via catalog) + // - Unknown = cannot verify (no catalog data or no bundle installed) + + if !hasCatalogData { + // When catalog is unavailable, set all to Unknown. + // BundleDeprecated uses Absent only when no bundle installed. + bundleReason := ocv1.ReasonAbsent + bundleMessage := "no bundle installed yet" + if installedBundleName != "" { + bundleReason = ocv1.ReasonDeprecationStatusUnknown + bundleMessage = "deprecation status unknown: catalog data unavailable" + } + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeDeprecated, + Status: metav1.ConditionUnknown, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: ext.GetGeneration(), + }) + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypePackageDeprecated, + Status: metav1.ConditionUnknown, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: ext.GetGeneration(), + }) + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeChannelDeprecated, + Status: metav1.ConditionUnknown, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: ext.GetGeneration(), + }) + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeBundleDeprecated, + Status: metav1.ConditionUnknown, + Reason: bundleReason, + Message: bundleMessage, + ObservedGeneration: ext.GetGeneration(), + }) + return + } + + // Handle catalog data available: set conditions to True when deprecated, False when not. + messages := slices.Concat(packageMessages, channelMessages, bundleMessages) + if len(messages) > 0 { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeDeprecated, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(messages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + } else { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonNotDeprecated, + Message: "not deprecated", + ObservedGeneration: ext.GetGeneration(), + }) + } + + if len(packageMessages) > 0 { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypePackageDeprecated, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(packageMessages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + } else { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypePackageDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonNotDeprecated, + Message: "package not deprecated", + ObservedGeneration: ext.GetGeneration(), + }) + } + + if len(channelMessages) > 0 { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeChannelDeprecated, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(channelMessages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + } else { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeChannelDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonNotDeprecated, + Message: "channel not deprecated", + ObservedGeneration: ext.GetGeneration(), + }) + } + + // BundleDeprecated: Unknown when no bundle installed, True when deprecated, False when not + if info.BundleStatus == metav1.ConditionUnknown { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeBundleDeprecated, + Status: metav1.ConditionUnknown, + Reason: ocv1.ReasonAbsent, + Message: "no bundle installed yet", + ObservedGeneration: ext.GetGeneration(), + }) + } else if len(bundleMessages) > 0 { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeBundleDeprecated, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonDeprecated, + Message: strings.Join(bundleMessages, "\n"), + ObservedGeneration: ext.GetGeneration(), + }) + } else { + SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeBundleDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonNotDeprecated, + Message: "bundle not deprecated", + ObservedGeneration: ext.GetGeneration(), + }) + } +} + +// isDeprecationCondition reports whether the given type is one of the deprecation +// conditions we manage separately. +func isDeprecationCondition(condType string) bool { + switch condType { + case ocv1.TypeDeprecated, ocv1.TypePackageDeprecated, ocv1.TypeChannelDeprecated, ocv1.TypeBundleDeprecated: + return true + default: + return false + } +} + +// deprecationInfo captures the deprecation data needed to update condition status. +type deprecationInfo struct { + PackageEntries []declcfg.DeprecationEntry + ChannelEntries []declcfg.DeprecationEntry + BundleEntries []declcfg.DeprecationEntry + BundleStatus metav1.ConditionStatus +} + +// buildDeprecationInfo filters the catalog deprecation data down to the package, channel, +// and bundle entries that matter for this ClusterExtension. An empty bundle name means +// nothing is installed yet, so we leave bundle status Unknown/Absent. +func buildDeprecationInfo(ext *ocv1.ClusterExtension, installedBundleName string, deprecation *declcfg.Deprecation) deprecationInfo { + info := deprecationInfo{BundleStatus: metav1.ConditionUnknown} channelSet := sets.New[string]() if ext.Spec.Source.Catalog != nil { - for _, channel := range ext.Spec.Source.Catalog.Channels { - channelSet.Insert(channel) - } + channelSet.Insert(ext.Spec.Source.Catalog.Channels...) } + if deprecation != nil { for _, entry := range deprecation.Entries { switch entry.Reference.Schema { case declcfg.SchemaPackage: - deprecations[ocv1.TypePackageDeprecated] = []declcfg.DeprecationEntry{entry} + info.PackageEntries = append(info.PackageEntries, entry) case declcfg.SchemaChannel: - if channelSet.Has(entry.Reference.Name) { - deprecations[ocv1.TypeChannelDeprecated] = append(deprecations[ocv1.TypeChannelDeprecated], entry) + // Include channel deprecations if: + // 1. No channels specified (channelSet empty) - any channel could be auto-selected + // 2. The deprecated channel matches one of the specified channels + if len(channelSet) == 0 || channelSet.Has(entry.Reference.Name) { + info.ChannelEntries = append(info.ChannelEntries, entry) } case declcfg.SchemaBundle: - if bundleName != entry.Reference.Name { - continue + if installedBundleName != "" && entry.Reference.Name == installedBundleName { + info.BundleEntries = append(info.BundleEntries, entry) } - deprecations[ocv1.TypeBundleDeprecated] = []declcfg.DeprecationEntry{entry} } } } - // first get ordered deprecation messages that we'll join in the Deprecated condition message - var deprecationMessages []string - for _, conditionType := range []string{ - ocv1.TypePackageDeprecated, - ocv1.TypeChannelDeprecated, - ocv1.TypeBundleDeprecated, - } { - if entries, ok := deprecations[conditionType]; ok { - for _, entry := range entries { - deprecationMessages = append(deprecationMessages, entry.Message) - } + // installedBundleName is empty when nothing is installed. In that case we want + // to report the bundle deprecation condition as Unknown/Absent. + if installedBundleName != "" { + if len(info.BundleEntries) > 0 { + info.BundleStatus = metav1.ConditionTrue + } else { + info.BundleStatus = metav1.ConditionFalse } } - // next, set the Deprecated condition - status, reason, message := metav1.ConditionFalse, ocv1.ReasonDeprecated, "" - if len(deprecationMessages) > 0 { - status, reason, message = metav1.ConditionTrue, ocv1.ReasonDeprecated, strings.Join(deprecationMessages, ";") - } - SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1.TypeDeprecated, - Reason: reason, - Status: status, - Message: message, - ObservedGeneration: ext.Generation, - }) - - // finally, set the individual deprecation conditions for package, channel, and bundle - for _, conditionType := range []string{ - ocv1.TypePackageDeprecated, - ocv1.TypeChannelDeprecated, - ocv1.TypeBundleDeprecated, - } { - entries, ok := deprecations[conditionType] - status, reason, message := metav1.ConditionFalse, ocv1.ReasonDeprecated, "" - if ok { - status, reason = metav1.ConditionTrue, ocv1.ReasonDeprecated - for _, entry := range entries { - message = fmt.Sprintf("%s\n%s", message, entry.Message) - } + return info +} + +// collectDeprecationMessages collects the non-empty deprecation messages from the provided entries. +func collectDeprecationMessages(entries []declcfg.DeprecationEntry) []string { + messages := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.Message != "" { + messages = append(messages, entry.Message) } - SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: conditionType, - Reason: reason, - Status: status, - Message: message, - ObservedGeneration: ext.Generation, - }) } + return messages } type ControllerBuilderOption func(builder *ctrl.Builder) @@ -304,6 +456,19 @@ func (r *ClusterExtensionReconciler) SetupWithManager(mgr ctrl.Manager, opts ... } func wrapErrorWithResolutionInfo(resolved ocv1.BundleMetadata, err error) error { + // Preserve TerminalError type and reason through wrapping + if errors.Is(err, reconcile.TerminalError(nil)) { + // Extract the reason if one was provided + reason, hasReason := errorutil.ExtractTerminalReason(err) + // Unwrap to get the inner error, add context + innerErr := errorutil.UnwrapTerminal(err) + wrappedErr := fmt.Errorf("error for resolved bundle %q with version %q: %w", resolved.Name, resolved.Version, innerErr) + // Re-wrap preserving the reason if it existed + if hasReason { + return errorutil.NewTerminalError(reason, wrappedErr) + } + return reconcile.TerminalError(wrappedErr) + } return fmt.Errorf("error for resolved bundle %q with version %q: %w", resolved.Name, resolved.Version, err) } diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index 1b09a43adf..28d766ace5 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -15,6 +15,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" + "k8s.io/apimachinery/pkg/api/equality" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -32,7 +33,8 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" - finalizers "github.com/operator-framework/operator-controller/internal/operator-controller/finalizers" + "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/labels" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" @@ -127,7 +129,7 @@ func TestClusterExtensionShortCircuitsReconcileDuringDeletion(t *testing.T) { func TestClusterExtensionResolutionFails(t *testing.T) { pkgName := fmt.Sprintf("non-existent-%s", rand.String(6)) cl, reconciler := newClientAndReconciler(t, func(d *deps) { - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { return nil, nil, nil, fmt.Errorf("no package %q found", pkgName) }) }) @@ -177,6 +179,262 @@ func TestClusterExtensionResolutionFails(t *testing.T) { require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) } +// TestClusterExtensionResolutionFailsWithDeprecationData verifies that deprecation warnings are shown even when resolution fails. +// +// Scenario: +// - Resolution fails (package not found or version not available) +// - Resolver returns deprecation data along with the error +// - Catalog has marked the package as deprecated +// - PackageDeprecated and Deprecated conditions show True with the deprecation message +// - BundleDeprecated stays Unknown/Absent because no bundle is installed yet +// +// This ensures deprecation warnings reach users even when installation cannot proceed. +func TestClusterExtensionResolutionFailsWithDeprecationData(t *testing.T) { + ctx := context.Background() + pkgName := fmt.Sprintf("deprecated-%s", rand.String(6)) + deprecationMessage := "package marked deprecated in catalog" + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + return nil, nil, &declcfg.Deprecation{ + Entries: []declcfg.DeprecationEntry{{ + Reference: declcfg.PackageScopedReference{Schema: declcfg.SchemaPackage}, + Message: deprecationMessage, + }}, + }, fmt.Errorf("no package %q found", pkgName) + }) + }) + + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + 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)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.EqualError(t, err, fmt.Sprintf("no package %q found", pkgName)) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond) + require.Equal(t, metav1.ConditionTrue, pkgCond.Status) + require.Equal(t, deprecationMessage, pkgCond.Message) + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionTrue, deprecatedCond.Status) + + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status, "no bundle installed yet, so keep it Unknown/Absent") + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + + verifyInvariants(ctx, t, reconciler.Client, clusterExtension) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) +} + +// TestClusterExtensionUpgradeShowsInstalledBundleDeprecation verifies that deprecation status +// reflects the INSTALLED bundle, not the RESOLVED bundle during upgrades. +// +// Scenario: +// - Bundle v1.0.0 is installed and deprecated in the catalog +// - Bundle v2.0.0 is available (resolved) and NOT deprecated +// - BundleDeprecated should show True with v1.0.0's deprecation message +// +// This demonstrates the key fix: status shows actual state (installed), not desired state (resolved). +// Users need to know what's currently running is deprecated, even if the upgrade target is fine. +func TestClusterExtensionUpgradeShowsInstalledBundleDeprecation(t *testing.T) { + ctx := context.Background() + pkgName := fmt.Sprintf("upgrade-%s", rand.String(6)) + installedBundleName := fmt.Sprintf("%s.v1.0.0", pkgName) + resolvedBundleName := fmt.Sprintf("%s.v2.0.0", pkgName) + deprecationMessage := "v1.0.0 is deprecated, please upgrade to v2.0.0" + + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + v := bundle.VersionRelease{ + Version: bsemver.MustParse("2.0.0"), + } + // Catalog has deprecation for v1.0.0 (installed), but v2.0.0 (resolved) is NOT deprecated + return &declcfg.Bundle{ + Name: resolvedBundleName, + Package: pkgName, + Image: fmt.Sprintf("quay.io/example/%s@sha256:resolved200", pkgName), + }, &v, &declcfg.Deprecation{ + Entries: []declcfg.DeprecationEntry{{ + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaBundle, + Name: installedBundleName, // v1.0.0 is deprecated + }, + Message: deprecationMessage, + }}, + }, nil + }) + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + Package: pkgName, + BundleMetadata: ocv1.BundleMetadata{ + Name: installedBundleName, // v1.0.0 installed + Version: "1.0.0", + }, + Image: fmt.Sprintf("quay.io/example/%s@sha256:installed100", pkgName), + }, + }, + } + d.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}} + d.Applier = &MockApplier{} + }) + + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + 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)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.NoError(t, err) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + // BundleDeprecated should reflect the INSTALLED bundle (v1.0.0), not the RESOLVED bundle (v2.0.0) + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionTrue, bundleCond.Status, "installed bundle v1.0.0 is deprecated") + require.Equal(t, ocv1.ReasonDeprecated, bundleCond.Reason) + require.Equal(t, deprecationMessage, bundleCond.Message) + + // Deprecated condition should also be True (combines all deprecation types) + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionTrue, deprecatedCond.Status) + require.Contains(t, deprecatedCond.Message, deprecationMessage) + + // Package and Channel should NOT be deprecated (not in deprecation data) + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond, "package is not deprecated, condition should be False") + require.Equal(t, metav1.ConditionFalse, pkgCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, pkgCond.Reason) + + channelCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, channelCond, "channel is not deprecated, condition should be False") + require.Equal(t, metav1.ConditionFalse, channelCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, channelCond.Reason) + + verifyInvariants(ctx, t, reconciler.Client, clusterExtension) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) +} + +// TestClusterExtensionResolutionFailsWithoutCatalogDeprecationData verifies deprecation status handling when catalog data is unavailable. +// +// Scenario: +// - A bundle is already installed (v1.0.0) +// - Catalog exists but resolution fails (transient catalog issue, e.g., updating) +// - Resolution error is returned with no deprecation data +// - All deprecation conditions must be set to Unknown (not False) +// - BundleDeprecated uses reason DeprecationStatusUnknown because catalog is unavailable +// +// This ensures users see "we don't know the deprecation status" rather than "definitely not deprecated" +// when the catalog source of truth is unavailable. +func TestClusterExtensionResolutionFailsWithoutCatalogDeprecationData(t *testing.T) { + ctx := context.Background() + pkgName := fmt.Sprintf("missing-%s", rand.String(6)) + catalogName := fmt.Sprintf("test-catalog-%s", rand.String(6)) + installedBundleName := fmt.Sprintf("%s.v1.0.0", pkgName) + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + return nil, nil, nil, fmt.Errorf("no bundles found for package %q", pkgName) + }) + + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + Package: pkgName, + BundleMetadata: ocv1.BundleMetadata{ + Name: installedBundleName, + Version: "1.0.0", + }, + Image: "example.com/installed@sha256:deadbeef", + }, + }, + } + }) + + // Create a ClusterCatalog so CheckCatalogsExist returns true, causing retry instead of fallback + catalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{Name: catalogName}, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: "quay.io/example/catalog:latest", + }, + }, + }, + } + require.NoError(t, cl.Create(ctx, catalog)) + + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + 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)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.EqualError(t, err, fmt.Sprintf("no bundles found for package %q", pkgName)) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + packageCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, packageCond) + require.Equal(t, metav1.ConditionUnknown, packageCond.Status) + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, packageCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", packageCond.Message) + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionUnknown, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, deprecatedCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", deprecatedCond.Message) + + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, bundleCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", bundleCond.Message) + + verifyInvariants(ctx, t, reconciler.Client, clusterExtension) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterCatalog{})) +} + func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { type testCase struct { name string @@ -230,7 +488,7 @@ func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { } }, func(d *deps) { - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -277,6 +535,25 @@ func TestClusterExtensionResolutionSuccessfulUnpackFails(t *testing.T) { require.Equal(t, expectReason, progressingCond.Reason) require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) + t.Log("By checking deprecation conditions remain neutral and bundle is Unknown when not installed") + // When not deprecated, conditions are False (following K8s conventions) + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond, "Deprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason) + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond, "PackageDeprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, pkgCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, pkgCond.Reason) + chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, chanCond, "ChannelDeprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, chanCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, chanCond.Reason) + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) }) } @@ -288,7 +565,7 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -361,6 +638,132 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T) require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason) require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version)) + t.Log("By checking deprecation conditions remain neutral and bundle is Unknown when not installed") + // When not deprecated, conditions are False (following K8s conventions) + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond, "Deprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, deprecatedCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, deprecatedCond.Reason) + pkgCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, pkgCond, "PackageDeprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, pkgCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, pkgCond.Reason) + chanCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, chanCond, "ChannelDeprecated condition should be False when not deprecated") + require.Equal(t, metav1.ConditionFalse, chanCond.Status) + require.Equal(t, ocv1.ReasonNotDeprecated, chanCond.Reason) + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status) + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) +} + +// TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors verifies deprecation status when apply fails. +// +// Scenario: +// - Resolution succeeds and returns a valid bundle (prometheus.v1.0.0) +// - Boxcutter applier fails during rollout (simulates apply failure) +// - A rolling revision exists but nothing is installed yet +// - Progressing condition shows the apply error (Retrying) +// - Deprecation conditions reflect catalog data (all False since nothing deprecated) +// - BundleDeprecated stays Unknown/Absent because apply failed before install +// +// This ensures apply errors appear in Progressing condition, not in deprecation conditions. +func TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors(t *testing.T) { + require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=true", features.BoxcutterRuntime))) + t.Cleanup(func() { + require.NoError(t, features.OperatorControllerFeatureGate.Set(fmt.Sprintf("%s=false", features.BoxcutterRuntime))) + }) + + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + // Boxcutter keeps a rolling revision when apply fails. We mirror that state so the test uses + // the same inputs the runtime would see. + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + RollingOut: []*controllers.RevisionMetadata{{}}, + }, + } + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + v := bundle.VersionRelease{ + Version: bsemver.MustParse("1.0.0"), + } + return &declcfg.Bundle{ + Name: "prometheus.v1.0.0", + Package: "prometheus", + Image: "quay.io/operatorhubio/prometheus@fake1.0.0", + }, &v, nil, nil + }) + d.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}} + d.Applier = &MockApplier{err: errors.New("boxcutter apply failure")} + }) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + + t.Log("When the Boxcutter Feature Flag is enabled and apply fails") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "prometheus", + Version: "1.0.0", + Channels: []string{"beta"}, + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{ + Name: "default", + }, + }, + } + require.NoError(t, cl.Create(ctx, clusterExtension)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + require.Error(t, err) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + + installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, installedCond) + require.Equal(t, metav1.ConditionFalse, installedCond.Status) + require.Equal(t, ocv1.ReasonAbsent, installedCond.Reason) + require.Contains(t, installedCond.Message, "No bundle installed") + + progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progressingCond) + require.Equal(t, metav1.ConditionTrue, progressingCond.Status) + require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason) + require.Contains(t, progressingCond.Message, "boxcutter apply failure") + + deprecatedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeDeprecated) + require.NotNil(t, deprecatedCond) + require.Equal(t, metav1.ConditionUnknown, deprecatedCond.Status, "no catalog data during rollout, so Unknown") + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, deprecatedCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", deprecatedCond.Message) + + packageCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypePackageDeprecated) + require.NotNil(t, packageCond) + require.Equal(t, metav1.ConditionUnknown, packageCond.Status, "no catalog data during rollout, so Unknown") + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, packageCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", packageCond.Message) + + channelCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeChannelDeprecated) + require.NotNil(t, channelCond) + require.Equal(t, metav1.ConditionUnknown, channelCond.Status, "no catalog data during rollout, so Unknown") + require.Equal(t, ocv1.ReasonDeprecationStatusUnknown, channelCond.Reason) + require.Equal(t, "deprecation status unknown: catalog data unavailable", channelCond.Message) + + bundleCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeBundleDeprecated) + require.NotNil(t, bundleCond) + require.Equal(t, metav1.ConditionUnknown, bundleCond.Status, "apply failed before install, so bundle status stays Unknown/Absent") + require.Equal(t, ocv1.ReasonAbsent, bundleCond.Reason) + require.Equal(t, "no bundle installed yet", bundleCond.Message) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) } @@ -428,7 +831,7 @@ func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) { d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -523,7 +926,7 @@ func TestClusterExtensionManagerFailed(t *testing.T) { d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -602,7 +1005,7 @@ func TestClusterExtensionManagedContentCacheWatchFail(t *testing.T) { d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -683,7 +1086,7 @@ func TestClusterExtensionInstallationSucceeds(t *testing.T) { d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -764,7 +1167,7 @@ func TestClusterExtensionDeleteFinalizerFails(t *testing.T) { d.ImagePuller = &imageutil.MockPuller{ ImageFS: fstest.MapFS{}, } - d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + d.Resolver = resolve.Func(func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { v := bundle.VersionRelease{ Version: bsemver.MustParse("1.0.0"), } @@ -867,14 +1270,24 @@ func verifyInvariants(ctx context.Context, t *testing.T, c client.Client, ext *o } func verifyConditionsInvariants(t *testing.T, ext *ocv1.ClusterExtension) { - // Expect that the cluster extension's set of conditions contains all defined - // condition types for the ClusterExtension API. Every reconcile should always - // ensure every condition type's status/reason/message reflects the state - // read during _this_ reconcile call. - require.Len(t, ext.Status.Conditions, len(conditionsets.ConditionTypes)) - for _, tt := range conditionsets.ConditionTypes { + // All conditions must always be present after first reconciliation. + // Core conditions: Installed, Progressing + // Deprecation conditions: Deprecated, PackageDeprecated, ChannelDeprecated, BundleDeprecated + coreConditions := []string{ocv1.TypeInstalled, ocv1.TypeProgressing} + deprecationConditions := []string{ocv1.TypeDeprecated, ocv1.TypePackageDeprecated, ocv1.TypeChannelDeprecated, ocv1.TypeBundleDeprecated} + + for _, tt := range coreConditions { + cond := apimeta.FindStatusCondition(ext.Status.Conditions, tt) + require.NotNil(t, cond, "core condition %s must be present", tt) + require.NotEmpty(t, cond.Status) + require.Contains(t, conditionsets.ConditionReasons, cond.Reason) + require.Equal(t, ext.GetGeneration(), cond.ObservedGeneration) + } + + // Deprecation conditions must always be present and valid + for _, tt := range deprecationConditions { cond := apimeta.FindStatusCondition(ext.Status.Conditions, tt) - require.NotNil(t, cond) + require.NotNil(t, cond, "deprecation condition %s must be present", tt) require.NotEmpty(t, cond.Status) require.Contains(t, conditionsets.ConditionReasons, cond.Reason) require.Equal(t, ext.GetGeneration(), cond.ObservedGeneration) @@ -882,15 +1295,30 @@ func verifyConditionsInvariants(t *testing.T, ext *ocv1.ClusterExtension) { } func TestSetDeprecationStatus(t *testing.T) { + // The catalogDataProvided/hasCatalogData pair lets each test express whether the catalog + // answered during reconciliation and, if it did, whether it marked anything as deprecated. + // This helps us cover three distinct user-facing states: "no catalog response" (everything + // stays Unknown), "catalog answered with no deprecations" (conditions explicitly set to + // False with reason NotDeprecated, with BundleDeprecated remaining Unknown when no bundle + // is installed), and "catalog answered with explicit deprecations" (conditions go True). + // + // Key scenarios tested: + // 1. No catalog data + no bundle -> all Unknown, BundleDeprecated uses reason Absent + // 2. No catalog data + bundle installed -> all Unknown, BundleDeprecated uses reason DeprecationStatusUnknown + // 3. Catalog data provided + no deprecations -> deprecation conditions explicitly set to False + // with reason NotDeprecated (BundleDeprecated remains Unknown when no bundle is installed) + // 4. Catalog data provided + explicit deprecations -> relevant conditions True for _, tc := range []struct { name string clusterExtension *ocv1.ClusterExtension expectedClusterExtension *ocv1.ClusterExtension bundle *declcfg.Bundle deprecation *declcfg.Deprecation + catalogDataProvided bool + hasCatalogData bool }{ { - name: "no deprecations, all deprecation statuses set to False", + name: "no catalog data, all deprecation statuses set to Unknown", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -907,36 +1335,183 @@ func TestSetDeprecationStatus(t *testing.T) { Conditions: []metav1.Condition{ { Type: ocv1.TypeDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypePackageDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypeChannelDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypeBundleDeprecated, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &declcfg.Bundle{}, + deprecation: nil, + catalogDataProvided: false, + hasCatalogData: false, + }, + { + // Scenario: + // - A bundle is installed (v1.0.0) + // - Catalog becomes unavailable (removed or network failure) + // - No catalog data can be retrieved + // - BundleDeprecated must show Unknown/DeprecationStatusUnknown (not Absent) + // - Reason is DeprecationStatusUnknown because catalog data is unavailable; Absent is only for no bundle + name: "no catalog data with installed bundle keeps bundle condition Unknown", + clusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{Conditions: []metav1.Condition{}}, + }, + expectedClusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Generation: 1}, + Status: ocv1.ClusterExtensionStatus{Conditions: []metav1.Condition{ + {Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecationStatusUnknown, Status: metav1.ConditionUnknown, Message: "deprecation status unknown: catalog data unavailable", ObservedGeneration: 1}, + {Type: ocv1.TypePackageDeprecated, Reason: ocv1.ReasonDeprecationStatusUnknown, Status: metav1.ConditionUnknown, Message: "deprecation status unknown: catalog data unavailable", ObservedGeneration: 1}, + {Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecationStatusUnknown, Status: metav1.ConditionUnknown, Message: "deprecation status unknown: catalog data unavailable", ObservedGeneration: 1}, + {Type: ocv1.TypeBundleDeprecated, Reason: ocv1.ReasonDeprecationStatusUnknown, Status: metav1.ConditionUnknown, Message: "deprecation status unknown: catalog data unavailable", ObservedGeneration: 1}, + }}, + }, + bundle: &declcfg.Bundle{Name: "installed.v1.0.0"}, + deprecation: nil, + catalogDataProvided: false, + hasCatalogData: false, + }, + { + // Scenario: + // - A bundle is installed + // - Catalog returns deprecation entries but catalogDataProvided=false + // - This tests that deprecation data is ignored when hasCatalogData is false + // - All conditions go to Unknown regardless of deprecation entries present + // - BundleDeprecated uses DeprecationStatusUnknown (not Absent) because bundle exists + name: "deprecation entries ignored when catalog data flag is false", + clusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1.TypeDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypePackageDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypeChannelDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + { + Type: ocv1.TypeBundleDeprecated, + Reason: ocv1.ReasonDeprecationStatusUnknown, + Status: metav1.ConditionUnknown, + Message: "deprecation status unknown: catalog data unavailable", + ObservedGeneration: 1, + }, + }, + }, + }, + bundle: &declcfg.Bundle{Name: "ignored"}, + deprecation: &declcfg.Deprecation{Entries: []declcfg.DeprecationEntry{{ + Reference: declcfg.PackageScopedReference{Schema: declcfg.SchemaPackage}, + Message: "should not surface", + }}}, + catalogDataProvided: true, + hasCatalogData: false, + }, + { + name: "catalog consulted but no deprecations, conditions False except BundleDeprecated Unknown when no bundle", + clusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + }, + expectedClusterExtension: &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{ + { + Type: ocv1.TypeDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "channel not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, }, }, - bundle: &declcfg.Bundle{}, - deprecation: nil, + bundle: &declcfg.Bundle{}, + deprecation: nil, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated channel, but no channel specified, all deprecation statuses set to False", + name: "deprecated channel exists, no channels specified (auto-select), channel deprecation shown", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -966,25 +1541,29 @@ func TestSetDeprecationStatus(t *testing.T) { { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, @@ -997,11 +1576,14 @@ func TestSetDeprecationStatus(t *testing.T) { Schema: declcfg.SchemaChannel, Name: "badchannel", }, + Message: "bad channel!", }}, }, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated channel, but a non-deprecated channel specified, all deprecation statuses set to False", + name: "deprecated channel exists but non-deprecated channel specified; conditions False except BundleDeprecated Unknown", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -1034,26 +1616,30 @@ func TestSetDeprecationStatus(t *testing.T) { Conditions: []metav1.Condition{ { Type: ocv1.TypeDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "channel not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, @@ -1070,9 +1656,11 @@ func TestSetDeprecationStatus(t *testing.T) { }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated channel specified, ChannelDeprecated and Deprecated status set to true, others set to false", + name: "deprecated channel specified, ChannelDeprecated and Deprecated set to true, PackageDeprecated False, BundleDeprecated Unknown", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -1107,24 +1695,28 @@ func TestSetDeprecationStatus(t *testing.T) { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, @@ -1142,6 +1734,8 @@ func TestSetDeprecationStatus(t *testing.T) { }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, { name: "deprecated package and channel specified, deprecated bundle, all deprecation statuses set to true", @@ -1179,24 +1773,28 @@ func TestSetDeprecationStatus(t *testing.T) { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad package!\nbad channel!\nbad bundle!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad package!", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad bundle!", ObservedGeneration: 1, }, }, @@ -1227,9 +1825,11 @@ func TestSetDeprecationStatus(t *testing.T) { }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated channel specified, deprecated bundle, all deprecation statuses set to true, all deprecation statuses set to true except PackageDeprecated", + name: "deprecated channel and bundle specified, Deprecated/ChannelDeprecated/BundleDeprecated set to true, PackageDeprecated False", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -1264,24 +1864,28 @@ func TestSetDeprecationStatus(t *testing.T) { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!\nbad bundle!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad bundle!", ObservedGeneration: 1, }, }, @@ -1306,9 +1910,11 @@ func TestSetDeprecationStatus(t *testing.T) { }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated package and channel specified, all deprecation statuses set to true except BundleDeprecated", + name: "deprecated package and channel specified, Deprecated/PackageDeprecated/ChannelDeprecated set to true, BundleDeprecated Unknown/Absent (no bundle installed)", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -1343,24 +1949,28 @@ func TestSetDeprecationStatus(t *testing.T) { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad package!\nbad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad package!", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, @@ -1384,9 +1994,11 @@ func TestSetDeprecationStatus(t *testing.T) { }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, { - name: "deprecated channels specified, ChannelDeprecated and Deprecated status set to true, others set to false", + name: "deprecated channels specified, ChannelDeprecated and Deprecated set to true, PackageDeprecated False, BundleDeprecated Unknown/Absent", clusterExtension: &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, @@ -1421,24 +2033,28 @@ func TestSetDeprecationStatus(t *testing.T) { Type: ocv1.TypeDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!\nanother bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypePackageDeprecated, - Reason: ocv1.ReasonDeprecated, + Reason: ocv1.ReasonNotDeprecated, Status: metav1.ConditionFalse, + Message: "package not deprecated", ObservedGeneration: 1, }, { Type: ocv1.TypeChannelDeprecated, Reason: ocv1.ReasonDeprecated, Status: metav1.ConditionTrue, + Message: "bad channel!\nanother bad channel!", ObservedGeneration: 1, }, { Type: ocv1.TypeBundleDeprecated, - Reason: ocv1.ReasonDeprecated, - Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + Status: metav1.ConditionUnknown, + Message: "no bundle installed yet", ObservedGeneration: 1, }, }, @@ -1459,21 +2075,232 @@ func TestSetDeprecationStatus(t *testing.T) { Schema: declcfg.SchemaChannel, Name: "anotherbadchannel", }, - Message: "another bad channedl!", + Message: "another bad channel!", }, }, }, + catalogDataProvided: true, + hasCatalogData: true, }, } { t.Run(tc.name, func(t *testing.T) { - controllers.SetDeprecationStatus(tc.clusterExtension, tc.bundle.Name, tc.deprecation) + // When a test provides deprecation data it must also explicitly state that the catalog responded. + // This guard keeps future cases from silently falling back to the "catalog absent" branch. + if tc.deprecation != nil && !tc.catalogDataProvided { + require.Failf(t, "test case must set catalogDataProvided when deprecation is supplied", "test case %q", tc.name) + } + hasCatalogData := tc.catalogDataProvided && tc.hasCatalogData + controllers.SetDeprecationStatus(tc.clusterExtension, tc.bundle.Name, tc.deprecation, hasCatalogData) // TODO: we should test for unexpected changes to lastTransitionTime. We only expect // lastTransitionTime to change when the status of the condition changes. - assert.Empty(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{}, "LastTransitionTime"))) + }) + } +} + +// TestSetDeprecationStatus_NoInfiniteReconcileLoop verifies that calling SetDeprecationStatus +// multiple times with the same inputs does not cause infinite reconciliation loops. +// +// The issue: If we always remove and re-add conditions, lastTransitionTime updates every time, +// which causes DeepEqual to fail, triggering another reconcile indefinitely. +// +// The fix: Only remove conditions when we're NOT re-adding them. When setting a condition, +// call SetStatusCondition directly - it preserves lastTransitionTime when status/reason/message +// haven't changed. +func TestSetDeprecationStatus_NoInfiniteReconcileLoop(t *testing.T) { + tests := []struct { + name string + installedBundleName string + deprecation *declcfg.Deprecation + hasCatalogData bool + setupConditions func(*ocv1.ClusterExtension) + expectConditionsCount int + description string + }{ + { + name: "deprecated package - should stabilize after first reconcile", + installedBundleName: "test.v1.0.0", + deprecation: &declcfg.Deprecation{ + Entries: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{ + Schema: declcfg.SchemaPackage, + }, + Message: "package is deprecated", + }, + }, + }, + hasCatalogData: true, + setupConditions: func(ext *ocv1.ClusterExtension) { + // No conditions initially + }, + expectConditionsCount: 4, // All 4 conditions: Deprecated/PackageDeprecated=True, ChannelDeprecated/BundleDeprecated=False + description: "First call adds conditions, second call preserves lastTransitionTime", + }, + { + name: "not deprecated - conditions always present as False", + installedBundleName: "", // No bundle installed + deprecation: nil, + hasCatalogData: true, + setupConditions: func(ext *ocv1.ClusterExtension) { + // Simulate old behavior: False conditions present with old reason + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypeDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonDeprecated, + Message: "", + ObservedGeneration: 1, + }) + apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ + Type: ocv1.TypePackageDeprecated, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonDeprecated, + Message: "", + ObservedGeneration: 1, + }) + }, + expectConditionsCount: 4, // All 4 conditions as False (except BundleDeprecated Unknown when no bundle) + description: "Sets all conditions to False with NotDeprecated reason, then stabilizes", + }, + { + name: "catalog unavailable - should stabilize with Unknown conditions", + installedBundleName: "test.v1.0.0", + deprecation: nil, + hasCatalogData: false, + setupConditions: func(ext *ocv1.ClusterExtension) { + // No conditions initially + }, + expectConditionsCount: 4, // All four Unknown conditions + description: "Sets Unknown conditions, then preserves them", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + } + + // Setup initial conditions if specified + if tt.setupConditions != nil { + tt.setupConditions(ext) + } + + // First reconcile: should add/update conditions + controllers.SetDeprecationStatus(ext, tt.installedBundleName, tt.deprecation, tt.hasCatalogData) + + firstReconcileConditions := make([]metav1.Condition, len(ext.Status.Conditions)) + copy(firstReconcileConditions, ext.Status.Conditions) + + // Verify expected number of conditions + deprecationConditions := filterDeprecationConditions(ext.Status.Conditions) + require.Len(t, deprecationConditions, tt.expectConditionsCount, + "First reconcile should have %d deprecation conditions", tt.expectConditionsCount) + + // Second reconcile: should preserve lastTransitionTime (no changes) + controllers.SetDeprecationStatus(ext, tt.installedBundleName, tt.deprecation, tt.hasCatalogData) + + secondReconcileConditions := ext.Status.Conditions + + // Verify conditions are identical (including lastTransitionTime) + require.Len(t, secondReconcileConditions, len(firstReconcileConditions), + "Number of conditions should remain the same") + + for i, firstCond := range firstReconcileConditions { + secondCond := secondReconcileConditions[i] + require.Equal(t, firstCond.Type, secondCond.Type, "Condition type should match") + require.Equal(t, firstCond.Status, secondCond.Status, "Condition status should match") + require.Equal(t, firstCond.Reason, secondCond.Reason, "Condition reason should match") + require.Equal(t, firstCond.Message, secondCond.Message, "Condition message should match") + + // This is the critical check: lastTransitionTime should NOT change + require.Equal(t, firstCond.LastTransitionTime, secondCond.LastTransitionTime, + "lastTransitionTime should be preserved (prevents infinite reconcile loop)") + } + + // Third reconcile: verify it remains stable + controllers.SetDeprecationStatus(ext, tt.installedBundleName, tt.deprecation, tt.hasCatalogData) + + thirdReconcileConditions := ext.Status.Conditions + require.Len(t, thirdReconcileConditions, len(secondReconcileConditions), + "Conditions should remain stable after multiple reconciles") + + for i, secondCond := range secondReconcileConditions { + thirdCond := thirdReconcileConditions[i] + require.Equal(t, secondCond.LastTransitionTime, thirdCond.LastTransitionTime, + "lastTransitionTime should remain stable across reconciles") + } }) } } +// TestSetDeprecationStatus_StatusChangesOnlyWhenNeeded verifies that calling SetDeprecationStatus +// only modifies the status when actual deprecation state changes, not on every reconcile. +func TestSetDeprecationStatus_StatusChangesOnlyWhenNeeded(t *testing.T) { + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + Status: ocv1.ClusterExtensionStatus{ + Conditions: []metav1.Condition{}, + }, + } + + // Scenario 1: Package becomes deprecated + deprecation := &declcfg.Deprecation{ + Entries: []declcfg.DeprecationEntry{ + { + Reference: declcfg.PackageScopedReference{Schema: declcfg.SchemaPackage}, + Message: "package is deprecated", + }, + }, + } + + // First reconcile: add deprecation condition + controllers.SetDeprecationStatus(ext, "test.v1.0.0", deprecation, true) + statusAfterFirstReconcile := ext.Status.DeepCopy() + + // Second reconcile: same deprecation state + controllers.SetDeprecationStatus(ext, "test.v1.0.0", deprecation, true) + statusAfterSecondReconcile := ext.Status.DeepCopy() + + // Status should be semantically equal (DeepEqual would return true) + require.True(t, equality.Semantic.DeepEqual(statusAfterFirstReconcile, statusAfterSecondReconcile), + "Status should not change when deprecation state is unchanged") + + // Scenario 2: Deprecation is resolved (package no longer deprecated) + controllers.SetDeprecationStatus(ext, "test.v1.0.0", nil, true) + statusAfterResolution := ext.Status.DeepCopy() + + // Status should have changed (conditions removed) + require.False(t, equality.Semantic.DeepEqual(statusAfterSecondReconcile, statusAfterResolution), + "Status should change when deprecation is resolved") + + // Scenario 3: Verify resolution is stable + controllers.SetDeprecationStatus(ext, "test.v1.0.0", nil, true) + statusAfterFourthReconcile := ext.Status.DeepCopy() + + require.True(t, equality.Semantic.DeepEqual(statusAfterResolution, statusAfterFourthReconcile), + "Status should remain stable after deprecation is resolved") +} + +// filterDeprecationConditions returns only the deprecation-related conditions +func filterDeprecationConditions(conditions []metav1.Condition) []metav1.Condition { + var result []metav1.Condition + for _, cond := range conditions { + switch cond.Type { + case ocv1.TypeDeprecated, ocv1.TypePackageDeprecated, ocv1.TypeChannelDeprecated, ocv1.TypeBundleDeprecated: + result = append(result, cond) + } + } + return result +} + type MockActionGetter struct { description string rels []*release.Release @@ -1611,3 +2438,443 @@ func TestGetInstalledBundleHistory(t *testing.T) { } } } + +// TestResolutionFallbackToInstalledBundle tests the catalog deletion resilience fallback logic +func TestResolutionFallbackToInstalledBundle(t *testing.T) { + t.Run("falls back when catalog unavailable and no version change", func(t *testing.T) { + resolveAttempt := 0 + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + // First reconcile: catalog available, second reconcile: catalog unavailable + d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + resolveAttempt++ + if resolveAttempt == 1 { + // First reconcile: catalog available, resolve to version 1.0.0 + v := bundle.VersionRelease{Version: bsemver.MustParse("1.0.0")} + return &declcfg.Bundle{ + Name: "test.1.0.0", + Package: "test-pkg", + Image: "test-image:1.0.0", + }, &v, &declcfg.Deprecation{}, nil + } + // Second reconcile: catalog unavailable + return nil, nil, nil, fmt.Errorf("catalog unavailable") + }) + // Applier succeeds (resources maintained) + d.Applier = &MockApplier{ + installCompleted: true, + installStatus: "", + err: nil, + } + d.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}} + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + Package: "test-pkg", + BundleMetadata: ocv1.BundleMetadata{Name: "test.1.0.0", Version: "1.0.0"}, + Image: "test-image:1.0.0", + }, + }, + } + }) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("test-%s", rand.String(8))} + + // Create ClusterExtension with no version specified + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + // No version - should fall back + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{Name: "default"}, + }, + } + require.NoError(t, cl.Create(ctx, ext)) + + // First reconcile: catalog available, install version 1.0.0 + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + + require.NoError(t, cl.Get(ctx, extKey, ext)) + require.Equal(t, "1.0.0", ext.Status.Install.Bundle.Version) + + // Verify status after first install + instCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, instCond.Reason) + + progCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, progCond.Reason) + + // Verify all conditions are present and valid after first reconcile + verifyInvariants(ctx, t, cl, ext) + + // Second reconcile: catalog unavailable, should fallback to installed version + // Catalog watch will trigger reconciliation when catalog becomes available again + res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + + // Verify status shows successful reconciliation after fallback + require.NoError(t, cl.Get(ctx, extKey, ext)) + + // Version should remain 1.0.0 (maintained from fallback) + require.Equal(t, "1.0.0", ext.Status.Install.Bundle.Version) + + // Progressing should be Succeeded (apply completed successfully) + progCond = apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, progCond.Reason) + + // Installed should be True (maintaining current version) + instCond = apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, instCond.Reason) + + // Verify all conditions remain valid after fallback + verifyInvariants(ctx, t, cl, ext) + }) + + t.Run("fails when version upgrade requested without catalog", func(t *testing.T) { + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + return nil, nil, nil, fmt.Errorf("catalog unavailable") + }) + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + BundleMetadata: ocv1.BundleMetadata{Name: "test.1.0.0", Version: "1.0.0"}, + }, + }, + } + }) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("test-%s", rand.String(8))} + + // Create ClusterExtension requesting version upgrade + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + Version: "1.0.1", // Requesting upgrade + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{Name: "default"}, + }, + } + require.NoError(t, cl.Create(ctx, ext)) + + // Reconcile should fail (can't upgrade without catalog) + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Error(t, err) + require.Equal(t, ctrl.Result{}, res) + + // Verify status shows Retrying + require.NoError(t, cl.Get(ctx, extKey, ext)) + + // Progressing should be Retrying (can't resolve without catalog) + progCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonRetrying, progCond.Reason) + + // Installed should be True (v1.0.0 is already installed per RevisionStatesGetter) + // but we can't upgrade to v1.0.1 without catalog + instCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + + // Verify all conditions are present and valid + verifyInvariants(ctx, t, cl, ext) + }) + + t.Run("auto-updates when catalog becomes available after fallback", func(t *testing.T) { + resolveAttempt := 0 + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + // First attempt: catalog unavailable, then becomes available + d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + resolveAttempt++ + if resolveAttempt == 1 { + // First reconcile: catalog unavailable + return nil, nil, nil, fmt.Errorf("catalog temporarily unavailable") + } + // Second reconcile (triggered by catalog watch): catalog available with new version + v := bundle.VersionRelease{Version: bsemver.MustParse("2.0.0")} + return &declcfg.Bundle{ + Name: "test.2.0.0", + Package: "test-pkg", + Image: "test-image:2.0.0", + }, &v, &declcfg.Deprecation{}, nil + }) + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + Package: "test-pkg", + BundleMetadata: ocv1.BundleMetadata{Name: "test.1.0.0", Version: "1.0.0"}, + Image: "test-image:1.0.0", + }, + }, + } + d.ImagePuller = &imageutil.MockPuller{ImageFS: fstest.MapFS{}} + d.Applier = &MockApplier{installCompleted: true} + }) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("test-%s", rand.String(8))} + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + // No version - auto-update to latest + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{Name: "default"}, + }, + } + require.NoError(t, cl.Create(ctx, ext)) + + // First reconcile: catalog unavailable, falls back to v1.0.0 + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + + require.NoError(t, cl.Get(ctx, extKey, ext)) + require.Equal(t, "1.0.0", ext.Status.Install.Bundle.Version) + + // Verify core status after fallback to installed version + instCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, instCond.Reason) + + progCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, progCond.Reason) + + // Note: When falling back without catalog access initially, deprecation conditions + // may not be set yet. Full validation happens after catalog is available. + + // Second reconcile: simulating catalog watch trigger, catalog now available with v2.0.0 + res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.NoError(t, err) + require.Equal(t, ctrl.Result{}, res) + + // Should have upgraded to v2.0.0 + require.NoError(t, cl.Get(ctx, extKey, ext)) + require.Equal(t, "2.0.0", ext.Status.Install.Bundle.Version) + + // Verify status after upgrade + instCond = apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, instCond.Reason) + + progCond = apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonSucceeded, progCond.Reason) + + // Verify all conditions remain valid after upgrade + verifyInvariants(ctx, t, cl, ext) + + // Verify resolution was attempted twice (fallback, then success) + require.Equal(t, 2, resolveAttempt) + }) + + t.Run("retries when catalogs exist but resolution fails", func(t *testing.T) { + cl, reconciler := newClientAndReconciler(t, func(d *deps) { + // Resolver fails (transient issue) + d.Resolver = resolve.Func(func(_ context.Context, _ *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bundle.VersionRelease, *declcfg.Deprecation, error) { + return nil, nil, nil, fmt.Errorf("transient catalog issue: cache stale") + }) + d.RevisionStatesGetter = &MockRevisionStatesGetter{ + RevisionStates: &controllers.RevisionStates{ + Installed: &controllers.RevisionMetadata{ + Package: "test-pkg", + BundleMetadata: ocv1.BundleMetadata{Name: "test.1.0.0", Version: "1.0.0"}, + Image: "test-image:1.0.0", + }, + }, + } + }) + + ctx := context.Background() + extKey := types.NamespacedName{Name: fmt.Sprintf("test-%s", rand.String(8))} + + // Create a ClusterCatalog matching the extension's selector + catalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: "test-registry/catalog:latest", + }, + }, + }, + } + require.NoError(t, cl.Create(ctx, catalog)) + + // Create ClusterExtension with no version specified + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + // No version specified + }, + }, + Namespace: "default", + ServiceAccount: ocv1.ServiceAccountReference{Name: "default"}, + }, + } + require.NoError(t, cl.Create(ctx, ext)) + + // Reconcile should fail and RETRY (not fallback) + // Catalogs exist, so this is likely a transient issue (catalog updating, cache stale, etc.) + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Error(t, err) + require.Equal(t, ctrl.Result{}, res) + + // Verify status shows Retrying (not falling back to installed bundle) + require.NoError(t, cl.Get(ctx, extKey, ext)) + + progCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progCond) + require.Equal(t, metav1.ConditionTrue, progCond.Status) + require.Equal(t, ocv1.ReasonRetrying, progCond.Reason) + require.Contains(t, progCond.Message, "transient catalog issue") + + // Installed should remain True (existing installation is maintained) + instCond := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, instCond) + require.Equal(t, metav1.ConditionTrue, instCond.Status) + + // Verify we did NOT fall back - status should show we're retrying + verifyInvariants(ctx, t, cl, ext) + + // Clean up the catalog so it doesn't affect other tests + require.NoError(t, cl.Delete(ctx, catalog)) + }) +} + +func TestCheckCatalogsExist(t *testing.T) { + t.Run("returns false when no catalogs exist", func(t *testing.T) { + cl := newClient(t) + ctx := context.Background() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + }, + }, + }, + } + + exists, err := controllers.CheckCatalogsExist(ctx, cl, ext) + require.NoError(t, err) + require.False(t, exists, "should return false when no catalogs exist") + }) + + t.Run("returns false when no selector provided", func(t *testing.T) { + cl := newClient(t) + ctx := context.Background() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + Selector: nil, // No selector + }, + }, + }, + } + + exists, err := controllers.CheckCatalogsExist(ctx, cl, ext) + require.NoError(t, err) + require.False(t, exists, "should return false when no catalogs exist (no selector)") + }) + + t.Run("returns false when empty selector provided", func(t *testing.T) { + cl := newClient(t) + ctx := context.Background() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + Selector: &metav1.LabelSelector{}, // Empty selector (matches everything) + }, + }, + }, + } + + exists, err := controllers.CheckCatalogsExist(ctx, cl, ext) + require.NoError(t, err, "empty selector should not cause error") + require.False(t, exists, "should return false when no catalogs exist (empty selector)") + }) + + t.Run("returns error for invalid selector", func(t *testing.T) { + cl := newClient(t) + ctx := context.Background() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test-pkg", + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "invalid", + Operator: "InvalidOperator", // Invalid operator + Values: []string{"value"}, + }, + }, + }, + }, + }, + }, + } + + exists, err := controllers.CheckCatalogsExist(ctx, cl, ext) + require.Error(t, err, "should return error for invalid selector") + require.Contains(t, err.Error(), "invalid catalog selector") + require.False(t, exists) + }) +} diff --git a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go index 09dd2ce7d2..16d691f8b0 100644 --- a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go +++ b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go @@ -19,10 +19,12 @@ package controllers import ( "context" "errors" + "fmt" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/finalizer" "sigs.k8s.io/controller-runtime/pkg/log" @@ -83,70 +85,275 @@ func RetrieveRevisionStates(r RevisionStatesGetter) ReconcileStepFunc { } } -func ResolveBundle(r resolve.Resolver) ReconcileStepFunc { +// ResolveBundle resolves the bundle to install or roll out for a ClusterExtension. +// It requires a controller-runtime client (in addition to the resolve.Resolver) to enable +// intelligent error handling when resolution fails. The client is used to check if ClusterCatalogs +// matching the extension's selector still exist in the cluster, allowing the controller to +// distinguish between "ClusterCatalog deleted" (fall back to installed bundle) and "transient failure" +// (retry resolution). This ensures workload resilience during ClusterCatalog outages while maintaining +// responsiveness during ClusterCatalog updates. +func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc { return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { l := log.FromContext(ctx) - var resolvedRevisionMetadata *RevisionMetadata - if len(state.revisionStates.RollingOut) == 0 { - l.Info("resolving bundle") - var bm *ocv1.BundleMetadata + + // If already rolling out, use existing revision and set deprecation to Unknown (no catalog check) + if len(state.revisionStates.RollingOut) > 0 { + installedBundleName := "" if state.revisionStates.Installed != nil { - bm = &state.revisionStates.Installed.BundleMetadata - } - resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolve(ctx, ext, bm) - if err != nil { - // Note: We don't distinguish between resolution-specific errors and generic errors - setStatusProgressing(ext, err) - setInstalledStatusFromRevisionStates(ext, state.revisionStates) - ensureAllConditionsWithReason(ext, ocv1.ReasonFailed, err.Error()) - return nil, err + installedBundleName = state.revisionStates.Installed.Name } + SetDeprecationStatus(ext, installedBundleName, nil, false) + state.resolvedRevisionMetadata = state.revisionStates.RollingOut[0] + return nil, nil + } - // 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, - // TODO: Right now, operator-controller only supports registry+v1 bundles and has no concept - // of a "release" field. If/when we add a release field concept or a new bundle format - // we need to re-evaluate use of `AsLegacyRegistryV1Version` so that we avoid propagating - // registry+v1's semver spec violations of treating build metadata as orderable. - BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, resolvedBundleVersion.AsLegacyRegistryV1Version()), - } - } else { - resolvedRevisionMetadata = state.revisionStates.RollingOut[0] + // Resolve a new bundle from the catalog + l.V(1).Info("resolving bundle") + var bm *ocv1.BundleMetadata + if state.revisionStates.Installed != nil { + bm = &state.revisionStates.Installed.BundleMetadata + } + resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolve(ctx, ext, bm) + + // Get the installed bundle name for deprecation status. + // BundleDeprecated should reflect what's currently running, not what we're trying to install. + installedBundleName := "" + if state.revisionStates.Installed != nil { + installedBundleName = state.revisionStates.Installed.Name + } + + // Set deprecation status based on resolution results: + // - If resolution succeeds: hasCatalogData=true, deprecation shows catalog data (nil=not deprecated) + // - If resolution fails but returns deprecation: hasCatalogData=true, show package/channel deprecation warnings + // - If resolution fails with nil deprecation: hasCatalogData=false, all conditions go Unknown + // + // Note: We DO check for deprecation data even when resolution fails (hasCatalogData = err == nil || resolvedDeprecation != nil). + // This allows us to show package/channel deprecation warnings even when we can't resolve a specific bundle. + // + // TODO: Open question - 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? This needs a follow-up discussion and PR. + hasCatalogData := err == nil || resolvedDeprecation != nil + SetDeprecationStatus(ext, installedBundleName, resolvedDeprecation, hasCatalogData) + + if err != nil { + return handleResolutionError(ctx, c, state, ext, err) + } + + state.resolvedRevisionMetadata = &RevisionMetadata{ + Package: resolvedBundle.Package, + Image: resolvedBundle.Image, + // TODO: Right now, operator-controller only supports registry+v1 bundles and has no concept + // of a "release" field. If/when we add a release field concept or a new bundle format + // we need to re-evaluate use of `AsLegacyRegistryV1Version` so that we avoid propagating + // registry+v1's semver spec violations of treating build metadata as orderable. + BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, resolvedBundleVersion.AsLegacyRegistryV1Version()), } - state.resolvedRevisionMetadata = resolvedRevisionMetadata return nil, nil } } +// handleResolutionError handles the case when bundle resolution fails. +// +// Decision logic (evaluated in order): +// 1. No installed bundle → Retry (cannot proceed without any bundle) +// 2. Version change requested → Retry (cannot upgrade without catalog) +// 3. Cannot check catalog existence → Retry (API error, cannot safely decide) +// 4. Catalogs exist → Retry (transient error, catalog may be updating) +// 5. Catalogs deleted → Fallback to installed bundle (maintain current state) +// +// When falling back (case 5), we set the resolved bundle to the installed bundle and return +// no error, allowing the Apply step to run and maintain resources using the existing installation. +// The controller watches ClusterCatalog resources, so reconciliation will automatically resume +// when catalogs return, enabling upgrades. +func handleResolutionError(ctx context.Context, c client.Client, state *reconcileState, ext *ocv1.ClusterExtension, err error) (*ctrl.Result, error) { + l := log.FromContext(ctx) + + // No installed bundle and resolution failed - cannot proceed + if state.revisionStates.Installed == nil { + msg := fmt.Sprintf("failed to resolve bundle: %v", err) + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + ensureFailureConditionsWithReason(ext, ocv1.ReasonRetrying, msg) + return nil, err + } + + // Check if the spec is requesting a specific version that differs from installed + specVersion := "" + if ext.Spec.Source.Catalog != nil { + specVersion = ext.Spec.Source.Catalog.Version + } + installedVersion := state.revisionStates.Installed.Version + + // If spec requests a different version, we cannot fall back - must fail and retry + if specVersion != "" && specVersion != installedVersion { + msg := fmt.Sprintf("unable to upgrade to version %s: %v (currently installed: %s)", specVersion, err, installedVersion) + l.Error(err, "resolution failed and spec requests version change - cannot fall back", + "requestedVersion", specVersion, + "installedVersion", installedVersion) + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + ensureFailureConditionsWithReason(ext, ocv1.ReasonRetrying, msg) + return nil, err + } + + // No version change requested - check if ClusterCatalogs exist + // Only fall back if ClusterCatalogs have been deleted + catalogsExist, catalogCheckErr := CheckCatalogsExist(ctx, c, ext) + if catalogCheckErr != nil { + msg := fmt.Sprintf("failed to resolve bundle: %v", err) + var catalogName string + if ext.Spec.Source.Catalog != nil { + catalogName = getCatalogNameFromSelector(ext.Spec.Source.Catalog.Selector) + } + l.Error(catalogCheckErr, "error checking if ClusterCatalogs exist, will retry resolution", + "resolutionError", err, + "packageName", getPackageName(ext), + "catalogName", catalogName) + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + ensureFailureConditionsWithReason(ext, ocv1.ReasonRetrying, msg) + return nil, err + } + + if catalogsExist { + // ClusterCatalogs exist but resolution failed - likely a transient issue (ClusterCatalog updating, cache stale, etc.) + // Retry resolution instead of falling back + msg := fmt.Sprintf("failed to resolve bundle, retrying: %v", err) + var catalogName string + if ext.Spec.Source.Catalog != nil { + catalogName = getCatalogNameFromSelector(ext.Spec.Source.Catalog.Selector) + } + l.Error(err, "resolution failed but matching ClusterCatalogs exist - retrying instead of falling back", + "packageName", getPackageName(ext), + "catalogName", catalogName) + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + ensureFailureConditionsWithReason(ext, ocv1.ReasonRetrying, msg) + return nil, err + } + + // ClusterCatalogs don't exist (deleted) - fall back to installed bundle to maintain current state. + // The controller watches ClusterCatalog resources, so when ClusterCatalogs become available again, + // a reconcile will be triggered automatically, allowing the extension to upgrade. + var catalogName string + if ext.Spec.Source.Catalog != nil { + catalogName = getCatalogNameFromSelector(ext.Spec.Source.Catalog.Selector) + } + l.Info("matching ClusterCatalogs unavailable or deleted - falling back to installed bundle to maintain workload", + "resolutionError", err.Error(), + "packageName", getPackageName(ext), + "catalogName", catalogName, + "installedBundle", state.revisionStates.Installed.Name, + "installedVersion", state.revisionStates.Installed.Version) + // Set installed status based on current revision states (needed before Apply runs) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + state.resolvedRevisionMetadata = state.revisionStates.Installed + // Return no error to allow Apply step to run and maintain resources. + // Apply will set Progressing=Succeeded when it completes successfully. + return nil, nil +} + +// getCatalogNameFromSelector extracts the catalog name from the selector if available. +// Returns empty string if selector is nil or doesn't contain the metadata.name label. +func getCatalogNameFromSelector(selector *metav1.LabelSelector) string { + if selector == nil || selector.MatchLabels == nil { + return "" + } + return selector.MatchLabels["olm.operatorframework.io/metadata.name"] +} + +// getPackageName safely extracts the package name from the extension spec. +// Returns empty string if Catalog source is nil. +func getPackageName(ext *ocv1.ClusterExtension) string { + if ext.Spec.Source.Catalog == nil { + return "" + } + return ext.Spec.Source.Catalog.PackageName +} + +// CheckCatalogsExist checks if any ClusterCatalogs matching the extension's selector exist. +// Returns true if at least one matching ClusterCatalog exists, false if none exist. +// Treats "CRD doesn't exist" errors as "no ClusterCatalogs exist" (returns false, nil). +// Returns an error only if the check itself fails unexpectedly. +func CheckCatalogsExist(ctx context.Context, c client.Client, ext *ocv1.ClusterExtension) (bool, error) { + var catalogList *ocv1.ClusterCatalogList + var listErr error + + if ext.Spec.Source.Catalog == nil || ext.Spec.Source.Catalog.Selector == nil { + // No selector means all ClusterCatalogs match - check if any ClusterCatalogs exist at all + catalogList = &ocv1.ClusterCatalogList{} + listErr = c.List(ctx, catalogList, client.Limit(1)) + } else { + // Convert label selector to k8slabels.Selector + // Note: An empty LabelSelector matches everything by default + selector, err := metav1.LabelSelectorAsSelector(ext.Spec.Source.Catalog.Selector) + if err != nil { + return false, fmt.Errorf("invalid catalog selector: %w", err) + } + + // List ClusterCatalogs matching the selector (limit to 1 since we only care if any exist) + catalogList = &ocv1.ClusterCatalogList{} + listErr = c.List(ctx, catalogList, client.MatchingLabelsSelector{Selector: selector}, client.Limit(1)) + } + + if listErr != nil { + // Check if the error is because the ClusterCatalog CRD doesn't exist + // This can happen if catalogd is not installed, which means no ClusterCatalogs exist + if apimeta.IsNoMatchError(listErr) { + return false, nil + } + return false, fmt.Errorf("failed to list ClusterCatalogs: %w", listErr) + } + + return len(catalogList.Items) > 0, nil +} + func UnpackBundle(i imageutil.Puller, cache imageutil.Cache) ReconcileStepFunc { return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { l := log.FromContext(ctx) - l.Info("unpacking resolved bundle") + + // Defensive check: resolvedRevisionMetadata should be set by ResolveBundle step + if state.resolvedRevisionMetadata == nil { + return nil, fmt.Errorf("unable to retrieve bundle information") + } + + // Always try to pull the bundle content (Pull uses cache-first strategy, so this is efficient) + l.V(1).Info("pulling bundle content") imageFS, _, _, err := i.Pull(ctx, ext.GetName(), state.resolvedRevisionMetadata.Image, cache) + + // Check if resolved bundle matches installed bundle (no version change) + bundleUnchanged := state.revisionStates != nil && + state.revisionStates.Installed != nil && + state.resolvedRevisionMetadata.Name == state.revisionStates.Installed.Name && + state.resolvedRevisionMetadata.Version == state.revisionStates.Installed.Version + 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 + if bundleUnchanged { + // Bundle hasn't changed and Pull failed (likely cache miss + catalog unavailable). + // This happens in fallback mode after catalog deletion. Set imageFS to nil so the + // applier can maintain the workload using existing Helm release or ClusterExtensionRevision. + l.V(1).Info("bundle content unavailable but version unchanged, maintaining current installation", + "bundle", state.resolvedRevisionMetadata.Name, + "version", state.resolvedRevisionMetadata.Version, + "error", err.Error()) + state.imageFS = nil + return nil, nil + } + // New bundle version but Pull failed - this is an error condition setStatusProgressing(ext, wrapErrorWithResolutionInfo(state.resolvedRevisionMetadata.BundleMetadata, err)) setInstalledStatusFromRevisionStates(ext, state.revisionStates) return nil, err } + + if bundleUnchanged { + l.V(1).Info("bundle unchanged, using cached content for resource reconciliation", + "bundle", state.resolvedRevisionMetadata.Name, + "version", state.resolvedRevisionMetadata.Version) + } + state.imageFS = imageFS return nil, nil } diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller.go b/internal/operator-controller/controllers/clusterextensionrevision_controller.go index f9c11c2ad2..775f958735 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "strings" + "sync" "time" appsv1 "k8s.io/api/apps/v1" @@ -27,6 +28,7 @@ import ( "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/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -43,9 +45,12 @@ const ( // ClusterExtensionRevisionReconciler actions individual snapshots of ClusterExtensions, // as part of the boxcutter integration. type ClusterExtensionRevisionReconciler struct { - Client client.Client - RevisionEngine RevisionEngine - TrackingCache trackingCache + Client client.Client + RevisionEngineFactory RevisionEngineFactory + TrackingCache trackingCache + // track if we have queued up the reconciliation that detects eventual progress deadline issues + // keys is revision UUID, value is boolean + progressDeadlineCheckInFlight sync.Map } type trackingCache interface { @@ -55,11 +60,6 @@ type trackingCache interface { 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 @@ -79,6 +79,25 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req reconciledRev := existingRev.DeepCopy() res, reconcileErr := c.reconcile(ctx, reconciledRev) + if pd := existingRev.Spec.ProgressDeadlineMinutes; pd > 0 { + cnd := meta.FindStatusCondition(reconciledRev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + isStillProgressing := cnd != nil && cnd.Status == metav1.ConditionTrue && cnd.Reason != ocv1.ReasonSucceeded + succeeded := meta.IsStatusConditionTrue(reconciledRev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeSucceeded) + // check if we reached the progress deadline only if the revision is still progressing and has not succeeded yet + if isStillProgressing && !succeeded { + timeout := time.Duration(pd) * time.Minute + if time.Since(existingRev.CreationTimestamp.Time) > timeout { + // progress deadline reached, reset any errors and stop reconciling this revision + markAsNotProgressing(reconciledRev, ocv1.ReasonProgressDeadlineExceeded, fmt.Sprintf("Revision has not rolled out for %d minutes.", pd)) + reconcileErr = nil + res = ctrl.Result{} + } else if _, found := c.progressDeadlineCheckInFlight.Load(existingRev.GetUID()); !found && reconcileErr == nil { + // if we haven't already queued up a reconcile to check for progress deadline, queue one up, but only once + c.progressDeadlineCheckInFlight.Store(existingRev.GetUID(), true) + res = ctrl.Result{RequeueAfter: timeout} + } + } + } // Do checks before any Update()s, as Update() may modify the resource structure! updateStatus := !equality.Semantic.DeepEqual(existingRev.Status, reconciledRev.Status) @@ -121,11 +140,19 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev return ctrl.Result{}, fmt.Errorf("converting to boxcutter revision: %v", err) } + revisionEngine, err := c.RevisionEngineFactory.CreateRevisionEngine(ctx, rev) + if err != nil { + setRetryingConditions(rev, err.Error()) + return ctrl.Result{}, fmt.Errorf("failed to create revision engine: %v", err) + } + + // + // Teardown + // if !rev.DeletionTimestamp.IsZero() || rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { - return c.teardown(ctx, rev, revision) + return c.teardown(ctx, revisionEngine, revision, rev) } - revVersion := rev.GetAnnotations()[labels.BundleVersionKey] // // Reconcile // @@ -139,7 +166,7 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev return ctrl.Result{}, werr } - rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...) + rres, err := revisionEngine.Reconcile(ctx, *revision, opts...) if err != nil { if rres != nil { // Log detailed reconcile reports only in debug mode (V(1)) to reduce verbosity. @@ -178,7 +205,8 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev } } - if !rres.InTransistion() { + revVersion := rev.GetAnnotations()[labels.BundleVersionKey] + if !rres.InTransition() { markAsProgressing(rev, ocv1.ReasonSucceeded, fmt.Sprintf("Revision %s has rolled out.", revVersion)) } else { markAsProgressing(rev, ocv1.ReasonRollingOut, fmt.Sprintf("Revision %s is rolling out.", revVersion)) @@ -222,8 +250,8 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev for _, ores := range pres.GetObjects() { // we probably want an AvailabilityProbeType and run through all of them independently of whether // the revision is complete or not - pr := ores.Probes()[boxcutter.ProgressProbeType] - if pr.Success { + pr := ores.ProbeResults()[boxcutter.ProgressProbeType] + if pr.Status == machinerytypes.ProbeStatusTrue { continue } @@ -250,38 +278,32 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev 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 { - if tres != nil { - l.V(1).Info("teardown failure report", "report", tres.String()) - } - markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error()) - return ctrl.Result{}, fmt.Errorf("revision teardown: %v", err) - } - - // Log detailed teardown reports only in debug mode (V(1)) to reduce verbosity. - l.V(1).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 { - markAsAvailableUnknown(rev, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error()) +func (c *ClusterExtensionRevisionReconciler) teardown( + ctx context.Context, + revisionEngine RevisionEngine, + revision *boxcutter.Revision, + cer *ocv1.ClusterExtensionRevision, +) (ctrl.Result, error) { + if err := c.TrackingCache.Free(ctx, cer); err != nil { + markAsAvailableUnknown(cer, ocv1.ClusterExtensionRevisionReasonReconciling, err.Error()) return ctrl.Result{}, fmt.Errorf("error stopping informers: %v", err) } - - // Ensure conditions are set before removing the finalizer when archiving - if rev.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived && markAsArchived(rev) { - return ctrl.Result{}, nil + if cer.Spec.LifecycleState == ocv1.ClusterExtensionRevisionLifecycleStateArchived { + tdres, err := revisionEngine.Teardown(ctx, *revision) + if err != nil { + setRetryingConditions(cer, fmt.Sprintf("error archiving: %v", err)) + return ctrl.Result{}, fmt.Errorf("error tearing down revision: %v", err) + } + if tdres != nil && !tdres.IsComplete() { + setRetryingConditions(cer, "tearing down revision") + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + // Ensure conditions are set before removing the finalizer when archiving + if markAsArchived(cer) { + return ctrl.Result{}, nil + } } - - if err := c.removeFinalizer(ctx, rev, clusterExtensionRevisionTeardownFinalizer); err != nil { + if err := c.removeFinalizer(ctx, cer, clusterExtensionRevisionTeardownFinalizer); err != nil { return ctrl.Result{}, fmt.Errorf("error removing teardown finalizer: %v", err) } return ctrl.Result{}, nil @@ -292,10 +314,29 @@ type Sourcerer interface { } func (c *ClusterExtensionRevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { + skipProgressDeadlineExceededPredicate := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + rev, ok := e.ObjectNew.(*ocv1.ClusterExtensionRevision) + if !ok { + return true + } + // allow deletions to happen + if !rev.DeletionTimestamp.IsZero() { + return true + } + if cnd := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing); cnd != nil && cnd.Status == metav1.ConditionFalse && cnd.Reason == ocv1.ReasonProgressDeadlineExceeded { + return false + } + return true + }, + } return ctrl.NewControllerManagedBy(mgr). For( &ocv1.ClusterExtensionRevision{}, - builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + builder.WithPredicates( + predicate.ResourceVersionChangedPredicate{}, + skipProgressDeadlineExceededPredicate, + ), ). WatchesRawSource( c.TrackingCache.Source( diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller_internal_test.go b/internal/operator-controller/controllers/clusterextensionrevision_controller_internal_test.go index b1c966892f..af4445c6c6 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller_internal_test.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller_internal_test.go @@ -152,7 +152,7 @@ func Test_ClusterExtensionRevisionReconciler_listPreviousRevisions(t *testing.T) previous, err := reconciler.listPreviousRevisions(t.Context(), currentRev) require.NoError(t, err) - var names []string + names := make([]string, 0, len(previous)) for _, rev := range previous { names = append(names, rev.GetName()) } diff --git a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go index e54827962a..10304bf632 100644 --- a/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go +++ b/internal/operator-controller/controllers/clusterextensionrevision_controller_test.go @@ -152,9 +152,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t objects: []machinery.ObjectResult{ mockObjectResult{ success: true, - probes: map[string]machinery.ObjectProbeResult{ + probes: machinerytypes.ProbeResultContainer{ boxcutter.ProgressProbeType: { - Success: true, + Status: machinerytypes.ProbeStatusTrue, }, }, }, @@ -170,9 +170,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) return obj }(), - probes: map[string]machinery.ObjectProbeResult{ + probes: machinerytypes.ProbeResultContainer{ boxcutter.ProgressProbeType: { - Success: false, + Status: machinerytypes.ProbeStatusFalse, Messages: []string{ "something bad happened", "something worse happened", @@ -198,9 +198,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) return obj }(), - probes: map[string]machinery.ObjectProbeResult{ + probes: machinerytypes.ProbeResultContainer{ boxcutter.ProgressProbeType: { - Success: false, + Status: machinerytypes.ProbeStatusFalse, Messages: []string{ "we have a problem", }, @@ -243,9 +243,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t objects: []machinery.ObjectResult{ mockObjectResult{ success: true, - probes: map[string]machinery.ObjectProbeResult{ - boxcutter.ProgressProbeType: { - Success: true, + probes: machinerytypes.ProbeResultContainer{ + boxcutter.ProgressProbeType: machinerytypes.ProbeResult{ + Status: machinerytypes.ProbeStatusTrue, }, }, }, @@ -261,9 +261,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) return obj }(), - probes: map[string]machinery.ObjectProbeResult{ - boxcutter.ProgressProbeType: { - Success: false, + probes: machinerytypes.ProbeResultContainer{ + boxcutter.ProgressProbeType: machinerytypes.ProbeResult{ + Status: machinerytypes.ProbeStatusFalse, Messages: []string{ "something bad happened", "something worse happened", @@ -289,9 +289,9 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t obj.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) return obj }(), - probes: map[string]machinery.ObjectProbeResult{ - boxcutter.ProgressProbeType: { - Success: false, + probes: machinerytypes.ProbeResultContainer{ + boxcutter.ProgressProbeType: machinerytypes.ProbeResult{ + Status: machinerytypes.ProbeStatusFalse, Messages: []string{ "we have a problem", }, @@ -484,14 +484,15 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_RevisionReconciliation(t 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, tc.revisionReconcileErr - }, + mockEngine := &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, tc.revisionReconcileErr }, - TrackingCache: &mockTrackingCache{client: testClient}, + } + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, + TrackingCache: &mockTrackingCache{client: testClient}, }).Reconcile(t.Context(), ctrl.Request{ NamespacedName: types.NamespacedName{ Name: tc.reconcilingRevisionName, @@ -603,14 +604,15 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_ValidationError_Retries(t 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 - }, + mockEngine := &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil }, - TrackingCache: &mockTrackingCache{client: testClient}, + } + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, + TrackingCache: &mockTrackingCache{client: testClient}, }).Reconcile(t.Context(), ctrl.Request{ NamespacedName: types.NamespacedName{ Name: clusterExtensionRevisionName, @@ -642,6 +644,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { validate func(*testing.T, client.Client) trackingCacheFreeFn func(context.Context, client.Object) error expectedErr string + expectedResult ctrl.Result }{ { name: "teardown finalizer is removed", @@ -652,7 +655,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { rev1.Finalizers = []string{ "olm.operatorframework.io/teardown", } - return []client.Object{rev1} + return []client.Object{ext, rev1} }, validate: func(t *testing.T, c client.Client) { rev := &ocv1.ClusterExtensionRevision{} @@ -696,7 +699,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { }, }, { - name: "set Available:Unknown:Reconciling and surface tear down errors when deleted", + name: "set Available:Unknown:Reconciling and surface tracking cache cleanup errors when deleted", revisionResult: mockRevisionResult{}, existingObjs: func() []client.Object { ext := newTestClusterExtension() @@ -709,10 +712,15 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { }, 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") + return &mockRevisionTeardownResult{ + isComplete: true, + }, nil } }, - expectedErr: "some teardown error", + trackingCacheFreeFn: func(ctx context.Context, object client.Object) error { + return fmt.Errorf("some tracking cache cleanup error") + }, + expectedErr: "some tracking cache cleanup error", validate: func(t *testing.T, c client.Client) { t.Log("cluster revision is not deleted and still contains finalizer") rev := &ocv1.ClusterExtensionRevision{} @@ -720,17 +728,16 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { Name: clusterExtensionRevisionName, }, rev) require.NoError(t, err) - require.NotContains(t, "olm.operatorframework.io/teardown", rev.Finalizers) cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) require.Equal(t, ocv1.ClusterExtensionRevisionReasonReconciling, cond.Reason) - require.Equal(t, "some teardown error", cond.Message) + require.Equal(t, "some tracking cache cleanup error", cond.Message) require.Equal(t, int64(1), cond.ObservedGeneration) }, }, { - name: "set Available:Unknown:Reconciling and surface tracking cache cleanup errors when deleted", + name: "set Available:Archived:Unknown and Progressing:False:Archived conditions when a revision is archived", revisionResult: mockRevisionResult{}, existingObjs: func() []client.Object { ext := newTestClusterExtension() @@ -738,7 +745,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { rev1.Finalizers = []string{ "olm.operatorframework.io/teardown", } - rev1.DeletionTimestamp = &metav1.Time{Time: time.Now()} + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived return []client.Object{rev1, ext} }, revisionEngineTeardownFn: func(t *testing.T) func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionTeardownOption) (machinery.RevisionTeardownResult, error) { @@ -748,12 +755,7 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { }, nil } }, - trackingCacheFreeFn: func(ctx context.Context, object client.Object) error { - return fmt.Errorf("some tracking cache cleanup error") - }, - expectedErr: "some tracking cache cleanup 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, @@ -762,13 +764,20 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) require.NotNil(t, cond) require.Equal(t, metav1.ConditionUnknown, cond.Status) - require.Equal(t, ocv1.ClusterExtensionRevisionReasonReconciling, cond.Reason) - require.Equal(t, "some tracking cache cleanup error", cond.Message) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) + require.Equal(t, "revision is archived", cond.Message) + require.Equal(t, int64(1), cond.ObservedGeneration) + + cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionFalse, 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: "set Available:Archived:Unknown and Progressing:False:Archived conditions when a revision is archived", + name: "set Progressing:True:Retrying and requeue when archived revision teardown is incomplete", revisionResult: mockRevisionResult{}, existingObjs: func() []client.Object { ext := newTestClusterExtension() @@ -782,29 +791,59 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { 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, + isComplete: false, }, nil } }, + expectedResult: ctrl.Result{RequeueAfter: 5 * time.Second}, 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) + cond := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) 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) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason) + require.Equal(t, "tearing down revision", cond.Message) - cond = meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + // Finalizer should still be present + require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown") + }, + }, + { + name: "return error and set retrying conditions when archived revision teardown fails", + revisionResult: mockRevisionResult{}, + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme) + rev1.Finalizers = []string{ + "olm.operatorframework.io/teardown", + } + rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + 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("teardown failed: connection refused") + } + }, + expectedErr: "error tearing down revision", + 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.ClusterExtensionRevisionTypeProgressing) require.NotNil(t, cond) - require.Equal(t, metav1.ConditionFalse, cond.Status) - require.Equal(t, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) - require.Equal(t, "revision is archived", cond.Message) - require.Equal(t, int64(1), cond.ObservedGeneration) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ocv1.ClusterExtensionRevisionReasonRetrying, cond.Reason) + require.Contains(t, cond.Message, "teardown failed: connection refused") + + // Finalizer should still be present + require.Contains(t, rev.Finalizers, "olm.operatorframework.io/teardown") }, }, { @@ -849,32 +888,145 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { require.NotContains(t, rev.Finalizers, "olm.operatorframework.io/teardown") }, }, + } { + 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 + mockEngine := &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil + }, + teardown: tc.revisionEngineTeardownFn(t), + } + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, + TrackingCache: &mockTrackingCache{ + client: testClient, + freeFn: tc.trackingCacheFreeFn, + }, + }).Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: clusterExtensionRevisionName, + }, + }) + + // reconcile cluster extension revision + require.Equal(t, tc.expectedResult, result) + if tc.expectedErr != "" { + require.Contains(t, err.Error(), tc.expectedErr) + } else { + require.NoError(t, err) + } + + // validate test case + tc.validate(t, testClient) + }) + } +} + +func Test_ClusterExtensionRevisionReconciler_Reconcile_ProgressDeadline(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 + validate func(*testing.T, client.Client) + reconcileErr error + reconcileResult ctrl.Result + }{ { - name: "surfaces revision teardown error when in archived state", - revisionResult: mockRevisionResult{}, + name: "progressing set to false when progress deadline is exceeded", existingObjs: func() []client.Object { ext := newTestClusterExtension() rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme) - rev1.Finalizers = []string{ - "olm.operatorframework.io/teardown", - } - rev1.Spec.LifecycleState = ocv1.ClusterExtensionRevisionLifecycleStateArchived + rev1.Spec.ProgressDeadlineMinutes = 1 + rev1.CreationTimestamp = metav1.NewTime(time.Now().Add(-61 * time.Second)) 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") - } + revisionResult: &mockRevisionResult{ + inTransition: true, }, - 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) + cnd := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + require.Equal(t, metav1.ConditionFalse, cnd.Status) + require.Equal(t, ocv1.ReasonProgressDeadlineExceeded, cnd.Reason) + }, + }, + { + name: "requeue after progressDeadline time for final progression deadline check", + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme) + rev1.Spec.ProgressDeadlineMinutes = 1 + rev1.CreationTimestamp = metav1.NewTime(time.Now()) + return []client.Object{rev1, ext} + }, + revisionResult: &mockRevisionResult{ + inTransition: true, + }, + reconcileResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + 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) + cnd := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + require.Equal(t, metav1.ConditionTrue, cnd.Status) + require.Equal(t, ocv1.ReasonRollingOut, cnd.Reason) + }, + }, + { + name: "no progression deadline checks on revision recovery", + existingObjs: func() []client.Object { + ext := newTestClusterExtension() + rev1 := newTestClusterExtensionRevision(t, clusterExtensionRevisionName, ext, testScheme) + rev1.Spec.ProgressDeadlineMinutes = 1 + rev1.CreationTimestamp = metav1.NewTime(time.Now().Add(-2 * time.Minute)) + meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + ObservedGeneration: rev1.Generation, + }) + meta.SetStatusCondition(&rev1.Status.Conditions, metav1.Condition{ + Type: ocv1.ClusterExtensionRevisionTypeSucceeded, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonSucceeded, + ObservedGeneration: rev1.Generation, + }) + return []client.Object{rev1, ext} + }, + revisionResult: &mockRevisionResult{ + inTransition: true, + }, + 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) + cnd := meta.FindStatusCondition(rev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + require.Equal(t, metav1.ConditionTrue, cnd.Status) + require.Equal(t, ocv1.ReasonRollingOut, cnd.Reason) }, }, } { @@ -887,33 +1039,25 @@ func Test_ClusterExtensionRevisionReconciler_Reconcile_Deletion(t *testing.T) { 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), + mockEngine := &mockRevisionEngine{ + reconcile: func(ctx context.Context, rev machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return tc.revisionResult, nil }, + } + result, err := (&controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, TrackingCache: &mockTrackingCache{ client: testClient, - freeFn: tc.trackingCacheFreeFn, }, }).Reconcile(t.Context(), ctrl.Request{ NamespacedName: types.NamespacedName{ Name: clusterExtensionRevisionName, }, }) + require.Equal(t, tc.reconcileResult, result) + require.Equal(t, tc.reconcileErr, err) - // 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) }) } @@ -952,10 +1096,12 @@ func newTestClusterExtensionRevision(t *testing.T, revisionName string, ext *ocv UID: types.UID(revisionName), Generation: int64(1), Annotations: map[string]string{ - labels.PackageNameKey: "some-package", - labels.BundleNameKey: "some-package.v1.0.0", - labels.BundleReferenceKey: "registry.io/some-repo/some-package:v1.0.0", - labels.BundleVersionKey: "1.0.0", + labels.PackageNameKey: "some-package", + labels.BundleNameKey: "some-package.v1.0.0", + labels.BundleReferenceKey: "registry.io/some-repo/some-package:v1.0.0", + labels.BundleVersionKey: "1.0.0", + labels.ServiceAccountNameKey: ext.Spec.ServiceAccount.Name, + labels.ServiceAccountNamespaceKey: ext.Spec.Namespace, }, Labels: map[string]string{ labels.OwnerNameKey: "test-ext", @@ -1001,6 +1147,19 @@ func (m mockRevisionEngine) Reconcile(ctx context.Context, rev machinerytypes.Re return m.reconcile(ctx, rev) } +// mockRevisionEngineFactory creates mock RevisionEngines for testing +type mockRevisionEngineFactory struct { + engine controllers.RevisionEngine + createErr error +} + +func (f *mockRevisionEngineFactory) CreateRevisionEngine(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (controllers.RevisionEngine, error) { + if f.createErr != nil { + return nil, f.createErr + } + return f.engine, nil +} + type mockRevisionResult struct { validationError *validation.RevisionValidationError phases []machinery.PhaseResult @@ -1018,7 +1177,7 @@ func (m mockRevisionResult) GetPhases() []machinery.PhaseResult { return m.phases } -func (m mockRevisionResult) InTransistion() bool { +func (m mockRevisionResult) InTransition() bool { return m.inTransition } @@ -1034,6 +1193,8 @@ func (m mockRevisionResult) String() string { return m.string } +var _ machinery.PhaseResult = &mockPhaseResult{} + type mockPhaseResult struct { name string validationError *validation.PhaseValidationError @@ -1056,7 +1217,7 @@ func (m mockPhaseResult) GetObjects() []machinery.ObjectResult { return m.objects } -func (m mockPhaseResult) InTransistion() bool { +func (m mockPhaseResult) InTransition() bool { return m.inTransition } @@ -1072,12 +1233,28 @@ func (m mockPhaseResult) String() string { return m.string } +var _ machinery.ObjectResult = &mockObjectResult{} + type mockObjectResult struct { - action machinery.Action - object machinery.Object - success bool - probes map[string]machinery.ObjectProbeResult - string string + action machinery.Action + object machinery.Object + success bool + complete bool + paused bool + probes machinerytypes.ProbeResultContainer + string string +} + +func (m mockObjectResult) ProbeResults() machinerytypes.ProbeResultContainer { + return m.probes +} + +func (m mockObjectResult) IsComplete() bool { + return m.complete +} + +func (m mockObjectResult) IsPaused() bool { + return m.paused } func (m mockObjectResult) Action() machinery.Action { @@ -1092,14 +1269,12 @@ 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 } +var _ machinery.RevisionTeardownResult = mockRevisionTeardownResult{} + type mockRevisionTeardownResult struct { phases []machinery.PhaseTeardownResult isComplete bool @@ -1161,3 +1336,116 @@ func (m *mockTrackingCache) Free(ctx context.Context, user client.Object) error } return nil } + +func Test_ClusterExtensionRevisionReconciler_getScopedClient_Errors(t *testing.T) { + testScheme := newScheme(t) + + t.Run("works with serviceAccount annotation and without owner label", func(t *testing.T) { + rev := &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rev-1", + Labels: map[string]string{}, + Annotations: map[string]string{ + labels.ServiceAccountNameKey: "test-sa", + labels.ServiceAccountNamespaceKey: "test-ns", + labels.BundleVersionKey: "1.0.0", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 1, + Phases: []ocv1.ClusterExtensionRevisionPhase{}, + }, + } + + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithStatusSubresource(&ocv1.ClusterExtensionRevision{}). + WithObjects(rev). + Build() + + mockEngine := &mockRevisionEngine{ + reconcile: func(ctx context.Context, r machinerytypes.Revision, opts ...machinerytypes.RevisionReconcileOption) (machinery.RevisionResult, error) { + return &mockRevisionResult{}, nil + }, + } + + reconciler := &controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: &mockRevisionEngineFactory{engine: mockEngine}, + TrackingCache: &mockTrackingCache{client: testClient}, + } + + _, err := reconciler.Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "test-rev-1"}, + }) + + require.NoError(t, err) + }) + + t.Run("missing serviceAccount annotation", func(t *testing.T) { + rev := &ocv1.ClusterExtensionRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rev-1", + Annotations: map[string]string{ + labels.BundleVersionKey: "1.0.0", + }, + }, + Spec: ocv1.ClusterExtensionRevisionSpec{ + LifecycleState: ocv1.ClusterExtensionRevisionLifecycleStateActive, + Revision: 1, + Phases: []ocv1.ClusterExtensionRevisionPhase{}, + }, + } + + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(rev). + Build() + + failingFactory := &mockRevisionEngineFactory{ + createErr: errors.New("missing serviceAccount name annotation"), + } + + reconciler := &controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: failingFactory, + TrackingCache: &mockTrackingCache{client: testClient}, + } + + _, err := reconciler.Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "test-rev-1"}, + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "serviceAccount") + }) + + t.Run("factory fails to create engine", func(t *testing.T) { + ext := newTestClusterExtension() + rev := newTestClusterExtensionRevision(t, "test-rev", ext, testScheme) + + testClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(ext, rev). + Build() + + failingFactory := &mockRevisionEngineFactory{ + createErr: errors.New("token getter failed"), + } + + reconciler := &controllers.ClusterExtensionRevisionReconciler{ + Client: testClient, + RevisionEngineFactory: failingFactory, + TrackingCache: &mockTrackingCache{client: testClient}, + } + + _, err := reconciler.Reconcile(t.Context(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "test-rev"}, + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to create revision engine") + require.Contains(t, err.Error(), "token getter failed") + }) +} diff --git a/internal/operator-controller/controllers/common_controller.go b/internal/operator-controller/controllers/common_controller.go index 7fafc7bb7b..cb6834c6b6 100644 --- a/internal/operator-controller/controllers/common_controller.go +++ b/internal/operator-controller/controllers/common_controller.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ocv1 "github.com/operator-framework/operator-controller/api/v1" + errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error" ) const ( @@ -56,11 +57,8 @@ func setInstalledStatusFromRevisionStates(ext *ocv1.ClusterExtension, revisionSt // Nothing is installed if revisionStates.Installed == nil { setInstallStatus(ext, nil) - if len(revisionStates.RollingOut) == 0 { - setInstalledStatusConditionFalse(ext, ocv1.ReasonFailed, "No bundle installed") - } else { - setInstalledStatusConditionFalse(ext, ocv1.ReasonAbsent, "No bundle installed") - } + reason := determineFailureReason(revisionStates.RollingOut) + setInstalledStatusConditionFalse(ext, reason, "No bundle installed") return } // Something is installed @@ -71,6 +69,45 @@ func setInstalledStatusFromRevisionStates(ext *ocv1.ClusterExtension, revisionSt setInstalledStatusConditionSuccess(ext, fmt.Sprintf("Installed bundle %s successfully", revisionStates.Installed.Image)) } +// determineFailureReason determines the appropriate reason for the Installed condition +// when no bundle is installed (Installed: False). +// +// Returns Failed when: +// - No rolling revisions exist (nothing to install) +// - The latest rolling revision has Reason: Retrying (indicates an error occurred) +// +// Returns Absent when: +// - Rolling revisions exist with the latest having Reason: RollingOut (healthy phased rollout in progress) +// +// Rationale: +// - Failed: Semantically indicates an error prevented installation +// - Absent: Semantically indicates "not there yet" (neutral state, e.g., during healthy rollout) +// - Retrying reason indicates an error (config validation, apply failure, etc.) +// - RollingOut reason indicates healthy progress (not an error) +// - Only the LATEST revision matters - old errors superseded by newer healthy revisions should not cause Failed +// +// Note: rollingRevisions are sorted in ascending order by Spec.Revision (oldest to newest), +// so the latest revision is the LAST element in the array. +func determineFailureReason(rollingRevisions []*RevisionMetadata) string { + if len(rollingRevisions) == 0 { + return ocv1.ReasonFailed + } + + // Check if the LATEST rolling revision indicates an error (Retrying reason) + // Latest revision is the last element in the array (sorted ascending by Spec.Revision) + latestRevision := rollingRevisions[len(rollingRevisions)-1] + progressingCond := apimeta.FindStatusCondition(latestRevision.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) + if progressingCond != nil && progressingCond.Reason == string(ocv1.ClusterExtensionRevisionReasonRetrying) { + // Retrying indicates an error occurred (config, apply, validation, etc.) + // Use Failed for semantic correctness: installation failed due to error + return ocv1.ReasonFailed + } + + // No error detected in latest revision - it's progressing healthily (RollingOut) or no conditions set + // Use Absent for neutral "not installed yet" state + return ocv1.ReasonAbsent +} + // setInstalledStatusConditionSuccess sets the installed status condition to success. func setInstalledStatusConditionSuccess(ext *ocv1.ClusterExtension, message string) { SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ @@ -119,12 +156,20 @@ func setStatusProgressing(ext *ocv1.ClusterExtension, err error) { if err != nil { progressingCond.Reason = ocv1.ReasonRetrying - progressingCond.Message = err.Error() + // Unwrap TerminalError to avoid "terminal error:" prefix in message + progressingCond.Message = errorutil.UnwrapTerminal(err).Error() } if errors.Is(err, reconcile.TerminalError(nil)) { progressingCond.Status = metav1.ConditionFalse - progressingCond.Reason = ocv1.ReasonBlocked + // Try to extract a specific reason from the terminal error. + // If the error was created with NewTerminalError(reason, err), use that reason. + // Otherwise, fall back to the generic "Blocked" reason. + if reason, ok := errorutil.ExtractTerminalReason(err); ok { + progressingCond.Reason = reason + } else { + progressingCond.Reason = ocv1.ReasonBlocked + } } 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 4d0a0536d1..3cc7575430 100644 --- a/internal/operator-controller/controllers/common_controller_test.go +++ b/internal/operator-controller/controllers/common_controller_test.go @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ocv1 "github.com/operator-framework/operator-controller/api/v1" + errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error" ) func TestSetStatusProgressing(t *testing.T) { @@ -46,14 +47,25 @@ func TestSetStatusProgressing(t *testing.T) { }, }, { - name: "non-nil ClusterExtension, terminal error, Progressing condition has status False with reason Blocked", + name: "non-nil ClusterExtension, terminal error without reason, Progressing condition has status False with reason Blocked", err: reconcile.TerminalError(errors.New("boom")), clusterExtension: &ocv1.ClusterExtension{}, expected: metav1.Condition{ Type: ocv1.TypeProgressing, Status: metav1.ConditionFalse, Reason: ocv1.ReasonBlocked, - Message: "terminal error: boom", + Message: "boom", + }, + }, + { + name: "non-nil ClusterExtension, terminal error with InvalidConfiguration reason, Progressing condition has status False with that reason", + err: errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, errors.New("missing required field")), + clusterExtension: &ocv1.ClusterExtension{}, + expected: metav1.Condition{ + Type: ocv1.TypeProgressing, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonInvalidConfiguration, + Message: "missing required field", }, }, } { @@ -146,7 +158,7 @@ func TestClusterExtensionDeprecationMessageTruncation(t *testing.T) { deprecationMessages = append(deprecationMessages, fmt.Sprintf("API version 'v1beta1' of resource 'customresources%d.example.com' is deprecated, use 'v1' instead", i)) } - longDeprecationMsg := strings.Join(deprecationMessages, "; ") + longDeprecationMsg := strings.Join(deprecationMessages, "\n") setInstalledStatusConditionUnknown(ext, longDeprecationMsg) cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) @@ -240,3 +252,160 @@ func TestSetStatusConditionWrapper(t *testing.T) { }) } } + +func TestSetInstalledStatusFromRevisionStates_ConfigValidationError(t *testing.T) { + tests := []struct { + name string + revisionStates *RevisionStates + expectedInstalledCond metav1.Condition + }{ + { + name: "no revisions at all - uses Failed", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: nil, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonFailed, + }, + }, + { + name: "rolling revision with error (Retrying) - uses Failed", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "some error occurred", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonFailed, + }, + }, + { + name: "multiple rolling revisions with one Retrying - uses Failed", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRollingOut, + Message: "Revision is rolling out", + }, + }, + }, + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "validation error occurred", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonFailed, + }, + }, + { + name: "rolling revision with RollingOut reason - uses Absent", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRollingOut, + Message: "Revision is rolling out", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + }, + }, + { + name: "old revision with Retrying superseded by latest healthy - uses Absent", + revisionStates: &RevisionStates{ + Installed: nil, + RollingOut: []*RevisionMetadata{ + { + RevisionName: "rev-1", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ClusterExtensionRevisionReasonRetrying, + Message: "old error that was superseded", + }, + }, + }, + { + RevisionName: "rev-2", + Conditions: []metav1.Condition{ + { + Type: ocv1.ClusterExtensionRevisionTypeProgressing, + Status: metav1.ConditionTrue, + Reason: ocv1.ReasonRollingOut, + Message: "Latest revision is rolling out healthy", + }, + }, + }, + }, + }, + expectedInstalledCond: metav1.Condition{ + Type: ocv1.TypeInstalled, + Status: metav1.ConditionFalse, + Reason: ocv1.ReasonAbsent, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ext", + Generation: 1, + }, + } + + setInstalledStatusFromRevisionStates(ext, tt.revisionStates) + + cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(t, cond) + require.Equal(t, tt.expectedInstalledCond.Status, cond.Status) + require.Equal(t, tt.expectedInstalledCond.Reason, cond.Reason) + }) + } +} diff --git a/internal/operator-controller/controllers/revision_engine_factory.go b/internal/operator-controller/controllers/revision_engine_factory.go new file mode 100644 index 0000000000..aa8a9b9ffc --- /dev/null +++ b/internal/operator-controller/controllers/revision_engine_factory.go @@ -0,0 +1,149 @@ +//go:build !standard + +// This file is excluded from standard builds because ClusterExtensionRevision +// is an experimental feature. Standard builds use Helm-based applier only. +// The experimental build includes BoxcutterRuntime which requires these factories +// for serviceAccount-scoped client creation and RevisionEngine instantiation. + +package controllers + +import ( + "context" + "fmt" + "net/http" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "pkg.package-operator.run/boxcutter/machinery" + machinerytypes "pkg.package-operator.run/boxcutter/machinery/types" + "pkg.package-operator.run/boxcutter/managedcache" + "pkg.package-operator.run/boxcutter/ownerhandling" + "pkg.package-operator.run/boxcutter/validation" + "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/authentication" + "github.com/operator-framework/operator-controller/internal/operator-controller/labels" +) + +// RevisionEngine defines the interface for reconciling and tearing down revisions. +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) +} + +// RevisionEngineFactory creates a RevisionEngine for a ClusterExtensionRevision. +type RevisionEngineFactory interface { + CreateRevisionEngine(ctx context.Context, rev *ocv1.ClusterExtensionRevision) (RevisionEngine, error) +} + +// defaultRevisionEngineFactory creates boxcutter RevisionEngines with serviceAccount-scoped clients. +type defaultRevisionEngineFactory struct { + Scheme *runtime.Scheme + TrackingCache managedcache.TrackingCache + DiscoveryClient discovery.CachedDiscoveryInterface + RESTMapper meta.RESTMapper + FieldOwnerPrefix string + BaseConfig *rest.Config + TokenGetter *authentication.TokenGetter +} + +// CreateRevisionEngine constructs a boxcutter RevisionEngine for the given ClusterExtensionRevision. +// It reads the ServiceAccount from annotations and creates a scoped client. +func (f *defaultRevisionEngineFactory) CreateRevisionEngine(_ context.Context, rev *ocv1.ClusterExtensionRevision) (RevisionEngine, error) { + saNamespace, saName, err := f.getServiceAccount(rev) + if err != nil { + return nil, err + } + + scopedClient, err := f.createScopedClient(saNamespace, saName) + if err != nil { + return nil, err + } + + return machinery.NewRevisionEngine( + machinery.NewPhaseEngine( + machinery.NewObjectEngine( + f.Scheme, f.TrackingCache, scopedClient, + ownerhandling.NewNative(f.Scheme), + machinery.NewComparator(ownerhandling.NewNative(f.Scheme), f.DiscoveryClient, f.Scheme, f.FieldOwnerPrefix), + f.FieldOwnerPrefix, f.FieldOwnerPrefix, + ), + validation.NewClusterPhaseValidator(f.RESTMapper, scopedClient), + ), + validation.NewRevisionValidator(), scopedClient, + ), nil +} + +func (f *defaultRevisionEngineFactory) getServiceAccount(rev *ocv1.ClusterExtensionRevision) (string, string, error) { + annotations := rev.GetAnnotations() + if annotations == nil { + return "", "", fmt.Errorf("revision %q is missing required annotations", rev.Name) + } + + saName := strings.TrimSpace(annotations[labels.ServiceAccountNameKey]) + saNamespace := strings.TrimSpace(annotations[labels.ServiceAccountNamespaceKey]) + + if len(saName) == 0 { + return "", "", fmt.Errorf("revision %q is missing ServiceAccount name annotation", rev.Name) + } + if len(saNamespace) == 0 { + return "", "", fmt.Errorf("revision %q is missing ServiceAccount namespace annotation", rev.Name) + } + + return saNamespace, saName, nil +} + +func (f *defaultRevisionEngineFactory) createScopedClient(namespace, serviceAccountName string) (client.Client, error) { + saConfig := rest.AnonymousClientConfig(f.BaseConfig) + saConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &authentication.TokenInjectingRoundTripper{ + Tripper: rt, + TokenGetter: f.TokenGetter, + Key: types.NamespacedName{ + Name: serviceAccountName, + Namespace: namespace, + }, + } + }) + + scopedClient, err := client.New(saConfig, client.Options{ + Scheme: f.Scheme, + }) + if err != nil { + return nil, fmt.Errorf("failed to create client for ServiceAccount %s/%s: %w", namespace, serviceAccountName, err) + } + + return scopedClient, nil +} + +// NewDefaultRevisionEngineFactory creates a new defaultRevisionEngineFactory. +func NewDefaultRevisionEngineFactory( + scheme *runtime.Scheme, + trackingCache managedcache.TrackingCache, + discoveryClient discovery.CachedDiscoveryInterface, + restMapper meta.RESTMapper, + fieldOwnerPrefix string, + baseConfig *rest.Config, + tokenGetter *authentication.TokenGetter, +) (RevisionEngineFactory, error) { + if baseConfig == nil { + return nil, fmt.Errorf("baseConfig is required but not provided") + } + if tokenGetter == nil { + return nil, fmt.Errorf("tokenGetter is required but not provided") + } + return &defaultRevisionEngineFactory{ + Scheme: scheme, + TrackingCache: trackingCache, + DiscoveryClient: discoveryClient, + RESTMapper: restMapper, + FieldOwnerPrefix: fieldOwnerPrefix, + BaseConfig: baseConfig, + TokenGetter: tokenGetter, + }, nil +} diff --git a/internal/operator-controller/controllers/suite_test.go b/internal/operator-controller/controllers/suite_test.go index 6258a5d307..57305a75fd 100644 --- a/internal/operator-controller/controllers/suite_test.go +++ b/internal/operator-controller/controllers/suite_test.go @@ -106,7 +106,7 @@ func newClientAndReconciler(t *testing.T, opts ...reconcilerOption) (client.Clie } reconciler.ReconcileSteps = []controllers.ReconcileStepFunc{controllers.HandleFinalizers(d.Finalizers), controllers.RetrieveRevisionStates(d.RevisionStatesGetter)} if r := d.Resolver; r != nil { - reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.ResolveBundle(r)) + reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.ResolveBundle(r, cl)) } if i := d.ImagePuller; i != nil { reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.UnpackBundle(i, d.ImageCache)) diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 1ad409e099..e0ddd64017 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -33,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, }, diff --git a/internal/operator-controller/labels/labels.go b/internal/operator-controller/labels/labels.go index 63bf0c4931..16f45ecbb3 100644 --- a/internal/operator-controller/labels/labels.go +++ b/internal/operator-controller/labels/labels.go @@ -1,10 +1,47 @@ package labels const ( - OwnerKindKey = "olm.operatorframework.io/owner-kind" - OwnerNameKey = "olm.operatorframework.io/owner-name" - PackageNameKey = "olm.operatorframework.io/package-name" - BundleNameKey = "olm.operatorframework.io/bundle-name" - BundleVersionKey = "olm.operatorframework.io/bundle-version" + // OwnerKindKey is the label key used to record the kind of the owner + // resource responsible for creating or managing a ClusterExtensionRevision. + OwnerKindKey = "olm.operatorframework.io/owner-kind" + + // OwnerNameKey is the label key used to record the name of the owner + // resource responsible for creating or managing a ClusterExtensionRevision. + OwnerNameKey = "olm.operatorframework.io/owner-name" + + // PackageNameKey is the label key used to record the package name + // associated with a ClusterExtensionRevision. + PackageNameKey = "olm.operatorframework.io/package-name" + + // BundleNameKey is the label key used to record the bundle name + // associated with a ClusterExtensionRevision. + BundleNameKey = "olm.operatorframework.io/bundle-name" + + // BundleVersionKey is the label key used to record the bundle version + // associated with a ClusterExtensionRevision. + BundleVersionKey = "olm.operatorframework.io/bundle-version" + + // BundleReferenceKey is the label key used to record an external reference + // (such as an image or catalog reference) to the bundle for a + // ClusterExtensionRevision. BundleReferenceKey = "olm.operatorframework.io/bundle-reference" + + // ServiceAccountNameKey is the annotation key used to record the name of + // the ServiceAccount configured on the owning ClusterExtension. It is + // applied as an annotation on ClusterExtensionRevision resources to + // capture which ServiceAccount was used for their lifecycle operations. + ServiceAccountNameKey = "olm.operatorframework.io/service-account-name" + + // ServiceAccountNamespaceKey is the annotation key used to record the + // namespace of the ServiceAccount configured on the owning + // ClusterExtension. It is applied as an annotation on + // ClusterExtensionRevision resources together with ServiceAccountNameKey + // so that the effective ServiceAccount identity used for + // ClusterExtensionRevision operations is preserved. + ServiceAccountNamespaceKey = "olm.operatorframework.io/service-account-namespace" + + // MigratedFromHelmKey is the label key used to mark ClusterExtensionRevisions + // that were created during migration from Helm releases. This label is used + // to distinguish migrated revisions from those created by normal Boxcutter operation. + MigratedFromHelmKey = "olm.operatorframework.io/migrated-from-helm" ) diff --git a/internal/operator-controller/resolve/catalog.go b/internal/operator-controller/resolve/catalog.go index f0d4da6fab..f4cb38071d 100644 --- a/internal/operator-controller/resolve/catalog.go +++ b/internal/operator-controller/resolve/catalog.go @@ -237,7 +237,7 @@ func (rei resolutionError) Error() string { sb.WriteString(fmt.Sprintf("in channels %v ", rei.Channels)) } - matchedCatalogs := []string{} + matchedCatalogs := make([]string, 0, len(rei.ResolvedBundles)) for _, r := range rei.ResolvedBundles { matchedCatalogs = append(matchedCatalogs, r.catalog) } diff --git a/internal/operator-controller/rukpak/bundle/README.md b/internal/operator-controller/rukpak/bundle/README.md new file mode 100644 index 0000000000..010cf46b7c --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/README.md @@ -0,0 +1,51 @@ +# Registry+v1 Bundle Configuration JSON Schema + +This directory contains the JSON schema for registry+v1 bundle configuration validation. + +## Overview + +The `registryv1bundleconfig.json` schema is used to validate the bundle configuration in the ClusterExtension's inline configuration. This includes: + +- `watchNamespace`: Controls which namespace(s) the operator watches for custom resources +- `deploymentConfig`: Customizes operator deployment (environment variables, resources, volumes, etc.) + +The `deploymentConfig` portion is based on OLM v0's `SubscriptionConfig` struct but excludes the `selector` field which was never used in v0. + +## Schema Generation + +The schema in `registryv1bundleconfig.json` is a frozen snapshot that provides stability for validation. It is based on the `v1alpha1.SubscriptionConfig` type from `github.com/operator-framework/api/pkg/operators/v1alpha1/subscription_types.go`. + +### Fields Included + +- `nodeSelector`: Map of node selector labels +- `tolerations`: Array of pod tolerations +- `resources`: Container resource requirements (requests/limits) +- `envFrom`: Environment variables from ConfigMaps/Secrets +- `env`: Individual environment variables +- `volumes`: Pod volumes +- `volumeMounts`: Container volume mounts +- `affinity`: Pod affinity/anti-affinity rules +- `annotations`: Custom annotations for deployments/pods + +### Fields Excluded + +- `selector`: This field exists in v0's `SubscriptionConfig` but is never used by the v0 controller. It has been intentionally excluded from the v1 schema. + +## Regenerating the Schema + +To regenerate the schema when the `github.com/operator-framework/api` dependency is updated: + +```bash +make update-registryv1-bundle-schema +``` + +This will regenerate the schema based on the current module-resolved version of `v1alpha1.SubscriptionConfig` from `github.com/operator-framework/api` (as determined via `go list -m`). + +## Validation + +The schema is used to validate user-provided bundle configuration (including `watchNamespace` and `deploymentConfig`) in ClusterExtension resources. The base schema is loaded and customized at runtime based on the operator's install modes to ensure proper validation of the `watchNamespace` field. Validation happens during: + +1. **Admission**: When a ClusterExtension is created or updated +2. **Runtime**: When extracting configuration from the inline field + +Validation errors provide clear, semantic feedback to users about what fields are invalid and why. diff --git a/internal/operator-controller/rukpak/bundle/registryv1.go b/internal/operator-controller/rukpak/bundle/registryv1.go index 7fc3e3e185..5b0c2a8cd9 100644 --- a/internal/operator-controller/rukpak/bundle/registryv1.go +++ b/internal/operator-controller/rukpak/bundle/registryv1.go @@ -1,6 +1,10 @@ package bundle import ( + _ "embed" + "encoding/json" + "fmt" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/sets" @@ -11,7 +15,14 @@ import ( ) const ( - BundleConfigWatchNamespaceKey = "watchNamespace" + watchNamespaceConfigKey = "watchNamespace" + namespaceNamePattern = "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + namespaceNameMaxLength = 63 +) + +var ( + //go:embed registryv1bundleconfig.json + bundleConfigSchemaJSON []byte ) type RegistryV1 struct { @@ -31,38 +42,91 @@ func (rv1 *RegistryV1) GetConfigSchema() (map[string]any, error) { return buildBundleConfigSchema(installModes) } -// buildBundleConfigSchema creates validation rules based on what the operator supports. +// buildBundleConfigSchema loads the base bundle config schema and modifies it based on +// the operator's install modes. // -// Examples of how install modes affect validation: -// - AllNamespaces only: user can't set watchNamespace (operator watches everything) -// - OwnNamespace only: user must set watchNamespace to the install namespace -// - SingleNamespace only: user must set watchNamespace to a different namespace -// - AllNamespaces + OwnNamespace: user can optionally set watchNamespace +// The base schema includes +// 1. watchNamespace +// 2. deploymentConfig properties. +// The watchNamespace property is modified based on what the operator supports: +// - AllNamespaces only: remove watchNamespace (operator always watches everything) +// - OwnNamespace only: make watchNamespace required, must equal install namespace +// - SingleNamespace only: make watchNamespace required, must differ from install namespace +// - AllNamespaces + OwnNamespace: make watchNamespace optional func buildBundleConfigSchema(installModes sets.Set[v1alpha1.InstallMode]) (map[string]any, error) { - schema := map[string]any{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "additionalProperties": false, // Reject unknown fields (catches typos and misconfigurations) + // Load the base schema + baseSchema, err := getBundleConfigSchemaMap() + if err != nil { + return nil, fmt.Errorf("failed to get base bundle config schema: %w", err) } - properties := map[string]any{} - var required []any + // Get properties map from the schema + properties, ok := baseSchema["properties"].(map[string]any) + if !ok { + return nil, fmt.Errorf("base schema missing properties") + } - // Add watchNamespace property if the bundle supports it + // Modify watchNamespace field based on install modes if isWatchNamespaceConfigurable(installModes) { + // Replace the generic watchNamespace with install-mode-specific version watchNSProperty, isRequired := buildWatchNamespaceProperty(installModes) - properties["watchNamespace"] = watchNSProperty + properties[watchNamespaceConfigKey] = watchNSProperty + + // Preserve existing required fields, only add/remove watchNamespace if isRequired { - required = append(required, "watchNamespace") + addToRequired(baseSchema, watchNamespaceConfigKey) + } else { + removeFromRequired(baseSchema, watchNamespaceConfigKey) } + } else { + // AllNamespaces only - remove watchNamespace property entirely + // (operator always watches all namespaces, no configuration needed) + delete(properties, watchNamespaceConfigKey) + removeFromRequired(baseSchema, watchNamespaceConfigKey) } - schema["properties"] = properties - if len(required) > 0 { - schema["required"] = required + return baseSchema, nil +} + +// addToRequired adds fieldName to the schema's required array if it's not already present. +// Preserves any existing required fields. +func addToRequired(schema map[string]any, fieldName string) { + var required []any + if existingRequired, ok := schema["required"].([]any); ok { + // Check if field is already required + for _, field := range existingRequired { + if field == fieldName { + return // Already required + } + } + required = existingRequired } + // Add the field to required list + schema["required"] = append(required, fieldName) +} - return schema, nil +// removeFromRequired removes fieldName from the schema's required array if present. +// Preserves all other required fields. +func removeFromRequired(schema map[string]any, fieldName string) { + existingRequired, ok := schema["required"].([]any) + if !ok { + return // No required array + } + + // Filter out the field + filtered := make([]any, 0, len(existingRequired)) + for _, field := range existingRequired { + if field != fieldName { + filtered = append(filtered, field) + } + } + + // Update or delete the required array + if len(filtered) > 0 { + schema["required"] = filtered + } else { + delete(schema, "required") + } } // buildWatchNamespaceProperty creates the validation rules for the watchNamespace field. @@ -75,37 +139,37 @@ func buildBundleConfigSchema(installModes sets.Set[v1alpha1.InstallMode]) (map[s // // Returns the validation rules and whether the field is required. func buildWatchNamespaceProperty(installModes sets.Set[v1alpha1.InstallMode]) (map[string]any, bool) { - watchNSProperty := map[string]any{ - "description": "The namespace that the operator should watch for custom resources", - } + const description = "The namespace that the operator should watch for custom resources" hasOwnNamespace := installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}) hasSingleNamespace := installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) format := selectNamespaceFormat(hasOwnNamespace, hasSingleNamespace) - if isWatchNamespaceConfigRequired(installModes) { - watchNSProperty["type"] = "string" - if format != "" { - watchNSProperty["format"] = format - } - return watchNSProperty, true - } - - // allow null or valid namespace string - stringSchema := map[string]any{ - "type": "string", + watchNamespaceSchema := map[string]any{ + "type": "string", + "minLength": 1, + "maxLength": namespaceNameMaxLength, + // kubernetes namespace name format + "pattern": namespaceNamePattern, } if format != "" { - stringSchema["format"] = format + watchNamespaceSchema["format"] = format } - // Convert to []any for JSON schema compatibility - watchNSProperty["anyOf"] = []any{ - map[string]any{"type": "null"}, - stringSchema, + + if isWatchNamespaceConfigRequired(installModes) { + watchNamespaceSchema["description"] = description + return watchNamespaceSchema, true } - return watchNSProperty, false + // allow null or valid namespace string + return map[string]any{ + "description": description, + "anyOf": []any{ + map[string]any{"type": "null"}, + watchNamespaceSchema, + }, + }, false } // selectNamespaceFormat picks which namespace constraint to apply. @@ -151,3 +215,16 @@ func isWatchNamespaceConfigRequired(installModes sets.Set[v1alpha1.InstallMode]) return isWatchNamespaceConfigurable(installModes) && !installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) } + +// getBundleConfigSchemaMap returns the complete registry+v1 bundle configuration schema +// as a map[string]any. This includes the following properties: +// 1. watchNamespace +// 2. deploymentConfig +// The schema can be modified at runtime based on operator install modes before validation. +func getBundleConfigSchemaMap() (map[string]any, error) { + var schemaMap map[string]any + if err := json.Unmarshal(bundleConfigSchemaJSON, &schemaMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal bundle config schema: %w", err) + } + return schemaMap, nil +} diff --git a/internal/operator-controller/rukpak/bundle/registryv1_test.go b/internal/operator-controller/rukpak/bundle/registryv1_test.go new file mode 100644 index 0000000000..6efcbafa21 --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/registryv1_test.go @@ -0,0 +1,119 @@ +package bundle + +import ( + "testing" + + "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetBundleConfigSchemaMap(t *testing.T) { + schema, err := getBundleConfigSchemaMap() + require.NoError(t, err, "should successfully get bundle config schema") + require.NotNil(t, schema, "schema should not be nil") + + t.Run("schema has correct metadata", func(t *testing.T) { + assert.Equal(t, "http://json-schema.org/draft-07/schema#", schema["$schema"]) + assert.Contains(t, schema["$id"], "registry-v1-bundle-config") + assert.Equal(t, "Registry+v1 Bundle Configuration", schema["title"]) + assert.NotEmpty(t, schema["description"]) + assert.Equal(t, "object", schema["type"]) + assert.Equal(t, false, schema["additionalProperties"]) + }) + + t.Run("schema includes watchNamespace and deploymentConfig properties", func(t *testing.T) { + properties, ok := schema["properties"].(map[string]any) + require.True(t, ok, "schema should have properties") + + assert.Contains(t, properties, "watchNamespace") + assert.Contains(t, properties, "deploymentConfig") + }) + + t.Run("watchNamespace has anyOf with null and string", func(t *testing.T) { + properties, ok := schema["properties"].(map[string]any) + require.True(t, ok) + + watchNamespace, ok := properties["watchNamespace"].(map[string]any) + require.True(t, ok, "watchNamespace should be present") + + anyOf, ok := watchNamespace["anyOf"].([]any) + require.True(t, ok, "watchNamespace should have anyOf") + assert.Len(t, anyOf, 2, "watchNamespace anyOf should have 2 options") + }) + + t.Run("deploymentConfig has expected structure", func(t *testing.T) { + properties, ok := schema["properties"].(map[string]any) + require.True(t, ok) + + deploymentConfig, ok := properties["deploymentConfig"].(map[string]any) + require.True(t, ok, "deploymentConfig should be present") + + assert.Equal(t, "object", deploymentConfig["type"]) + assert.Equal(t, false, deploymentConfig["additionalProperties"]) + assert.NotEmpty(t, deploymentConfig["description"]) + + dcProps, ok := deploymentConfig["properties"].(map[string]any) + require.True(t, ok, "deploymentConfig should have properties") + + // Verify expected fields from SubscriptionConfig + expectedFields := []string{ + "nodeSelector", + "tolerations", + "resources", + "env", + "envFrom", + "volumes", + "volumeMounts", + "affinity", + "annotations", + } + + for _, field := range expectedFields { + assert.Contains(t, dcProps, field, "deploymentConfig should include %s field", field) + } + + // Verify selector is NOT included + assert.NotContains(t, dcProps, "selector", "selector field should be excluded per RFC") + }) + + t.Run("schema includes components/schemas for OpenAPI types", func(t *testing.T) { + components, ok := schema["components"].(map[string]any) + require.True(t, ok, "schema should have components section for $ref resolution") + + schemas, ok := components["schemas"].(map[string]any) + require.True(t, ok, "components should have schemas") + assert.NotEmpty(t, schemas, "components/schemas should not be empty") + + // Verify some expected Kubernetes types are included + expectedTypes := []string{ + "io.k8s.api.core.v1.Toleration", + "io.k8s.api.core.v1.ResourceRequirements", + "io.k8s.api.core.v1.EnvVar", + } + + for _, typeName := range expectedTypes { + assert.Contains(t, schemas, typeName, "components/schemas should include %s", typeName) + } + }) +} + +// TestSchemaCompilation verifies that the generated schema can be compiled +// by a JSON schema validator without errors. This catches broken $ref targets +// and other structural issues. +func TestSchemaCompilation(t *testing.T) { + // Get the schema as a map (same as how config package uses it) + schemaMap, err := getBundleConfigSchemaMap() + require.NoError(t, err, "should successfully get bundle config schema") + + // Compile the schema using the same library used by config package + compiler := jsonschema.NewCompiler() + + // Add the schema resource (using map[string]any, same as config package) + err = compiler.AddResource("schema.json", schemaMap) + require.NoError(t, err, "should add schema resource to compiler") + + compiledSchema, err := compiler.Compile("schema.json") + require.NoError(t, err, "schema should compile without errors - this verifies all $ref targets are resolvable") + require.NotNil(t, compiledSchema, "compiled schema should not be nil") +} diff --git a/internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json b/internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json new file mode 100644 index 0000000000..1197f721a3 --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/registryv1bundleconfig.json @@ -0,0 +1,2657 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://operator-framework.io/schemas/registry-v1-bundle-config.json", + "title": "Registry+v1 Bundle Configuration", + "description": "Configuration schema for registry+v1 bundles. Includes watchNamespace for controlling operator scope and deploymentConfig for customizing operator deployment (environment variables, resource scheduling, storage, and pod placement). The deploymentConfig follows the same structure and behavior as OLM v0's SubscriptionConfig. Note: The 'selector' field from v0's SubscriptionConfig is not included as it was never used.", + "type": "object", + "properties": { + "deploymentConfig": { + "type": "object", + "description": "Configuration for customizing operator deployment (environment variables, resources, volumes, etc.)", + "properties": { + "affinity": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.Affinity" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "env": { + "type": "array", + "items": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVar" + } + }, + "envFrom": { + "type": "array", + "items": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvFromSource" + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "resources": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceRequirements" + }, + "tolerations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.Toleration" + } + }, + "volumeMounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeMount" + } + }, + "volumes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/io.k8s.api.core.v1.Volume" + } + } + }, + "additionalProperties": false + }, + "watchNamespace": { + "description": "The namespace that the operator should watch for custom resources. The meaning and validation of this field depends on the operator's install modes. This field may be optional or required, and may have format constraints, based on the operator's supported install modes.", + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + }, + "additionalProperties": false, + "components": { + "schemas": { + "io.k8s.api.core.v1.AWSElasticBlockStoreVolumeSource": { + "description": "Represents a Persistent Disk resource in AWS.\n\nAn AWS EBS disk must exist before mounting to a container. The disk must also be in the same AWS zone as the kubelet. An AWS EBS disk can only be mounted as read/write once. AWS EBS volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": "string" + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty).", + "format": "int32", + "type": "integer" + }, + "readOnly": { + "description": "readOnly value true will force the readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": "boolean" + }, + "volumeID": { + "default": "", + "description": "volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": "object" + }, + "io.k8s.api.core.v1.Affinity": { + "description": "Affinity is a group of affinity scheduling rules.", + "properties": { + "nodeAffinity": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeAffinity" + } + ], + "description": "Describes node affinity scheduling rules for the pod." + }, + "podAffinity": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinity" + } + ], + "description": "Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s))." + }, + "podAntiAffinity": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAntiAffinity" + } + ], + "description": "Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s))." + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.AzureDiskVolumeSource": { + "description": "AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.", + "properties": { + "cachingMode": { + "default": "ReadWrite", + "description": "cachingMode is the Host Caching mode: None, Read Only, Read Write.", + "type": "string" + }, + "diskName": { + "default": "", + "description": "diskName is the Name of the data disk in the blob storage", + "type": "string" + }, + "diskURI": { + "default": "", + "description": "diskURI is the URI of data disk in the blob storage", + "type": "string" + }, + "fsType": { + "default": "ext4", + "description": "fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "kind": { + "default": "Shared", + "description": "kind expected values are Shared: multiple blob disks per storage account Dedicated: single blob disk per storage account Managed: azure managed data disk (only in managed availability set). defaults to shared", + "type": "string" + }, + "readOnly": { + "default": false, + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + } + }, + "required": [ + "diskName", + "diskURI" + ], + "type": "object" + }, + "io.k8s.api.core.v1.AzureFileVolumeSource": { + "description": "AzureFile represents an Azure File Service mount on the host and bind mount to the pod.", + "properties": { + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "secretName": { + "default": "", + "description": "secretName is the name of secret that contains Azure Storage Account Name and Key", + "type": "string" + }, + "shareName": { + "default": "", + "description": "shareName is the azure share Name", + "type": "string" + } + }, + "required": [ + "secretName", + "shareName" + ], + "type": "object" + }, + "io.k8s.api.core.v1.CSIVolumeSource": { + "description": "Represents a source location of a volume to mount, managed by an external CSI driver", + "properties": { + "driver": { + "default": "", + "description": "driver is the name of the CSI driver that handles this volume. Consult with your admin for the correct name as registered in the cluster.", + "type": "string" + }, + "fsType": { + "description": "fsType to mount. Ex. \"ext4\", \"xfs\", \"ntfs\". If not provided, the empty value is passed to the associated CSI driver which will determine the default filesystem to apply.", + "type": "string" + }, + "nodePublishSecretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "nodePublishSecretRef is a reference to the secret object containing sensitive information to pass to the CSI driver to complete the CSI NodePublishVolume and NodeUnpublishVolume calls. This field is optional, and may be empty if no secret is required. If the secret object contains more than one secret, all secret references are passed." + }, + "readOnly": { + "description": "readOnly specifies a read-only configuration for the volume. Defaults to false (read/write).", + "type": "boolean" + }, + "volumeAttributes": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "description": "volumeAttributes stores driver-specific properties that are passed to the CSI driver. Consult your driver's documentation for supported values.", + "type": "object" + } + }, + "required": [ + "driver" + ], + "type": "object" + }, + "io.k8s.api.core.v1.CephFSVolumeSource": { + "description": "Represents a Ceph Filesystem mount that lasts the lifetime of a pod Cephfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "monitors": { + "description": "monitors is Required: Monitors is a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "path": { + "description": "path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /", + "type": "string" + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": "boolean" + }, + "secretFile": { + "description": "secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": "string" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it" + }, + "user": { + "description": "user is optional: User is the rados user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": "string" + } + }, + "required": [ + "monitors" + ], + "type": "object" + }, + "io.k8s.api.core.v1.CinderVolumeSource": { + "description": "Represents a cinder volume resource in Openstack. A Cinder volume must exist before mounting to a container. The volume must also be in the same region as the kubelet. Cinder volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": "string" + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef is optional: points to a secret object containing parameters used to connect to OpenStack." + }, + "volumeID": { + "default": "", + "description": "volumeID used to identify the volume in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ClusterTrustBundleProjection": { + "description": "ClusterTrustBundleProjection describes how to select a set of ClusterTrustBundle objects and project their contents into the pod filesystem.", + "properties": { + "labelSelector": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" + } + ], + "description": "Select all ClusterTrustBundles that match this label selector. Only has effect if signerName is set. Mutually-exclusive with name. If unset, interpreted as \"match nothing\". If set but empty, interpreted as \"match everything\"." + }, + "name": { + "description": "Select a single ClusterTrustBundle by object name. Mutually-exclusive with signerName and labelSelector.", + "type": "string" + }, + "optional": { + "description": "If true, don't block pod startup if the referenced ClusterTrustBundle(s) aren't available. If using name, then the named ClusterTrustBundle is allowed not to exist. If using signerName, then the combination of signerName and labelSelector is allowed to match zero ClusterTrustBundles.", + "type": "boolean" + }, + "path": { + "default": "", + "description": "Relative path from the volume root to write the bundle.", + "type": "string" + }, + "signerName": { + "description": "Select all ClusterTrustBundles that match this signer name. Mutually-exclusive with name. The contents of all selected ClusterTrustBundles will be unified and deduplicated.", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ConfigMapEnvSource": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": "boolean" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.ConfigMapKeySelector": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "default": "", + "description": "The key to select.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.ConfigMapProjection": { + "description": "Adapts a ConfigMap into a projected volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a projected volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. Note that this is identical to a configmap volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": "boolean" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.ConfigMapVolumeSource": { + "description": "Adapts a ConfigMap into a volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. ConfigMap volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": "boolean" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.DownwardAPIProjection": { + "description": "Represents downward API info for projecting into a projected volume. Note that this is identical to a downwardAPI volume source without the default mode.", + "properties": { + "items": { + "description": "Items is a list of DownwardAPIVolume file", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeFile" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.DownwardAPIVolumeFile": { + "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", + "properties": { + "fieldRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ObjectFieldSelector" + } + ], + "description": "Required: Selects a field of the pod: only annotations, labels, name, namespace and uid are supported." + }, + "mode": { + "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "path": { + "default": "", + "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", + "type": "string" + }, + "resourceFieldRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceFieldSelector" + } + ], + "description": "Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported." + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.DownwardAPIVolumeSource": { + "description": "DownwardAPIVolumeSource represents a volume containing downward API info. Downward API volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "Optional: mode bits to use on created files by default. Must be a Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "items": { + "description": "Items is a list of downward API volume file", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeFile" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.EmptyDirVolumeSource": { + "description": "Represents an empty directory for a pod. Empty directory volumes support ownership management and SELinux relabeling.", + "properties": { + "medium": { + "description": "medium represents what type of storage medium should back this directory. The default is \"\" which means to use the node's default medium. Must be an empty string (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", + "type": "string" + }, + "sizeLimit": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + } + ], + "description": "sizeLimit is the total amount of local storage required for this EmptyDir volume. The size limit is also applicable for memory medium. The maximum usage on memory medium EmptyDir would be the minimum value between the SizeLimit specified here and the sum of memory limits of all containers in a pod. The default is nil which means that the limit is undefined. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.EnvFromSource": { + "description": "EnvFromSource represents the source of a set of ConfigMaps or Secrets", + "properties": { + "configMapRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapEnvSource" + } + ], + "description": "The ConfigMap to select from" + }, + "prefix": { + "description": "Optional text to prepend to the name of each environment variable. May consist of any printable ASCII characters except '='.", + "type": "string" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretEnvSource" + } + ], + "description": "The Secret to select from" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.EnvVar": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "default": "", + "description": "Name of the environment variable. May consist of any printable ASCII characters except '='.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": "string" + }, + "valueFrom": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVarSource" + } + ], + "description": "Source for the environment variable's value. Cannot be used if value is not empty." + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "io.k8s.api.core.v1.EnvVarSource": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapKeySelector" + } + ], + "description": "Selects a key of a ConfigMap." + }, + "fieldRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ObjectFieldSelector" + } + ], + "description": "Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['\u003cKEY\u003e']`, `metadata.annotations['\u003cKEY\u003e']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs." + }, + "fileKeyRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.FileKeySelector" + } + ], + "description": "FileKeyRef selects a key of the env file. Requires the EnvFiles feature gate to be enabled." + }, + "resourceFieldRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceFieldSelector" + } + ], + "description": "Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported." + }, + "secretKeyRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretKeySelector" + } + ], + "description": "Selects a key of a secret in the pod's namespace" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.EphemeralVolumeSource": { + "description": "Represents an ephemeral volume that is handled by a normal storage driver.", + "properties": { + "volumeClaimTemplate": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimTemplate" + } + ], + "description": "Will be used to create a stand-alone PVC to provision the volume. The pod in which this EphemeralVolumeSource is embedded will be the owner of the PVC, i.e. the PVC will be deleted together with the pod. The name of the PVC will be `\u003cpod name\u003e-\u003cvolume name\u003e` where `\u003cvolume name\u003e` is the name from the `PodSpec.Volumes` array entry. Pod validation will reject the pod if the concatenated name is not valid for a PVC (for example, too long).\n\nAn existing PVC with that name that is not owned by the pod will *not* be used for the pod to avoid using an unrelated volume by mistake. Starting the pod is then blocked until the unrelated PVC is removed. If such a pre-created PVC is meant to be used by the pod, the PVC has to updated with an owner reference to the pod once the pod exists. Normally this should not be necessary, but it may be useful when manually reconstructing a broken cluster.\n\nThis field is read-only and no changes will be made by Kubernetes to the PVC after it has been created.\n\nRequired, must not be nil." + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.FCVolumeSource": { + "description": "Represents a Fibre Channel volume. Fibre Channel volumes can only be mounted as read/write once. Fibre Channel volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "lun": { + "description": "lun is Optional: FC target lun number", + "format": "int32", + "type": "integer" + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "targetWWNs": { + "description": "targetWWNs is Optional: FC target worldwide names (WWNs)", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "wwids": { + "description": "wwids Optional: FC volume world wide identifiers (wwids) Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously.", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.FileKeySelector": { + "description": "FileKeySelector selects a key of the env file.", + "properties": { + "key": { + "default": "", + "description": "The key within the env file. An invalid key will prevent the pod from starting. The keys defined within a source may consist of any printable ASCII characters except '='. During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters.", + "type": "string" + }, + "optional": { + "default": false, + "description": "Specify whether the file or its key must be defined. If the file or key does not exist, then the env var is not published. If optional is set to true and the specified key does not exist, the environment variable will not be set in the Pod's containers.\n\nIf optional is set to false and the specified key does not exist, an error will be returned during Pod creation.", + "type": "boolean" + }, + "path": { + "default": "", + "description": "The path within the volume from which to select the file. Must be relative and may not contain the '..' path or start with '..'.", + "type": "string" + }, + "volumeName": { + "default": "", + "description": "The name of the volume mount containing the env file.", + "type": "string" + } + }, + "required": [ + "volumeName", + "path", + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.FlexVolumeSource": { + "description": "FlexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin.", + "properties": { + "driver": { + "default": "", + "description": "driver is the name of the driver to use for this volume.", + "type": "string" + }, + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". The default filesystem depends on FlexVolume script.", + "type": "string" + }, + "options": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "description": "options is Optional: this field holds extra command options if any.", + "type": "object" + }, + "readOnly": { + "description": "readOnly is Optional: defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef is Optional: secretRef is reference to the secret object containing sensitive information to pass to the plugin scripts. This may be empty if no secret object is specified. If the secret object contains more than one secret, all secrets are passed to the plugin scripts." + } + }, + "required": [ + "driver" + ], + "type": "object" + }, + "io.k8s.api.core.v1.FlockerVolumeSource": { + "description": "Represents a Flocker volume mounted by the Flocker agent. One and only one of datasetName and datasetUUID should be set. Flocker volumes do not support ownership management or SELinux relabeling.", + "properties": { + "datasetName": { + "description": "datasetName is Name of the dataset stored as metadata -\u003e name on the dataset for Flocker should be considered as deprecated", + "type": "string" + }, + "datasetUUID": { + "description": "datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.GCEPersistentDiskVolumeSource": { + "description": "Represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once or read-only many times. GCE PDs support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": "string" + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "format": "int32", + "type": "integer" + }, + "pdName": { + "default": "", + "description": "pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": "boolean" + } + }, + "required": [ + "pdName" + ], + "type": "object" + }, + "io.k8s.api.core.v1.GitRepoVolumeSource": { + "description": "Represents a volume that is populated with the contents of a git repository. Git repo volumes do not support ownership management. Git repo volumes support SELinux relabeling.\n\nDEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.", + "properties": { + "directory": { + "description": "directory is the target directory name. Must not contain or start with '..'. If '.' is supplied, the volume directory will be the git repository. Otherwise, if specified, the volume will contain the git repository in the subdirectory with the given name.", + "type": "string" + }, + "repository": { + "default": "", + "description": "repository is the URL", + "type": "string" + }, + "revision": { + "description": "revision is the commit hash for the specified revision.", + "type": "string" + } + }, + "required": [ + "repository" + ], + "type": "object" + }, + "io.k8s.api.core.v1.GlusterfsVolumeSource": { + "description": "Represents a Glusterfs mount that lasts the lifetime of a pod. Glusterfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "endpoints": { + "default": "", + "description": "endpoints is the endpoint name that details Glusterfs topology.", + "type": "string" + }, + "path": { + "default": "", + "description": "path is the Glusterfs volume path. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the Glusterfs volume to be mounted with read-only permissions. Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "boolean" + } + }, + "required": [ + "endpoints", + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.HostPathVolumeSource": { + "description": "Represents a host path mapped into a pod. Host path volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "default": "", + "description": "path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": "string" + }, + "type": { + "description": "type for HostPath Volume Defaults to \"\" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ISCSIVolumeSource": { + "description": "Represents an ISCSI disk. ISCSI volumes can only be mounted as read/write once. ISCSI volumes support ownership management and SELinux relabeling.", + "properties": { + "chapAuthDiscovery": { + "description": "chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication", + "type": "boolean" + }, + "chapAuthSession": { + "description": "chapAuthSession defines whether support iSCSI Session CHAP authentication", + "type": "boolean" + }, + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi", + "type": "string" + }, + "initiatorName": { + "description": "initiatorName is the custom iSCSI Initiator Name. If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface \u003ctarget portal\u003e:\u003cvolume name\u003e will be created for the connection.", + "type": "string" + }, + "iqn": { + "default": "", + "description": "iqn is the target iSCSI Qualified Name.", + "type": "string" + }, + "iscsiInterface": { + "default": "default", + "description": "iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp).", + "type": "string" + }, + "lun": { + "default": 0, + "description": "lun represents iSCSI Target Lun number.", + "format": "int32", + "type": "integer" + }, + "portals": { + "description": "portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false.", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef is the CHAP Secret for iSCSI target and initiator authentication" + }, + "targetPortal": { + "default": "", + "description": "targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "type": "string" + } + }, + "required": [ + "targetPortal", + "iqn", + "lun" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ImageVolumeSource": { + "description": "ImageVolumeSource represents a image volume resource.", + "properties": { + "pullPolicy": { + "description": "Policy for pulling OCI objects. Possible values are: Always: the kubelet always attempts to pull the reference. Container creation will fail If the pull fails. Never: the kubelet never pulls the reference and only uses a local image or artifact. Container creation will fail if the reference isn't present. IfNotPresent: the kubelet pulls if the reference isn't already present on disk. Container creation will fail if the reference isn't present and the pull fails. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise.", + "type": "string" + }, + "reference": { + "description": "Required: Image or artifact reference to be used. Behaves in the same way as pod.spec.containers[*].image. Pull secrets will be assembled in the same way as for the container image by looking up node credentials, SA image pull secrets, and pod spec image pull secrets. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.KeyToPath": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "default": "", + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "path": { + "default": "", + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.LocalObjectReference": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.NFSVolumeSource": { + "description": "Represents an NFS mount that lasts the lifetime of a pod. NFS volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "default": "", + "description": "path that is exported by the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the NFS export to be mounted with read-only permissions. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "boolean" + }, + "server": { + "default": "", + "description": "server is the hostname or IP address of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + } + }, + "required": [ + "server", + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.NodeAffinity": { + "description": "Node affinity is a group of node affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PreferredSchedulingTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelector" + } + ], + "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to an update), the system may or may not try to eventually evict the pod from its node." + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.NodeSelector": { + "description": "A node selector represents the union of the results of one or more label queries over a set of nodes; that is, it represents the OR of the selectors represented by the node selector terms.", + "properties": { + "nodeSelectorTerms": { + "description": "Required. A list of node selector terms. The terms are ORed.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "required": [ + "nodeSelectorTerms" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.NodeSelectorRequirement": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "default": "", + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "default": "", + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "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. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "required": [ + "key", + "operator" + ], + "type": "object" + }, + "io.k8s.api.core.v1.NodeSelectorTerm": { + "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", + "properties": { + "matchExpressions": { + "description": "A list of node selector requirements by node's labels.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorRequirement" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "matchFields": { + "description": "A list of node selector requirements by node's fields.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorRequirement" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.ObjectFieldSelector": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": "string" + }, + "fieldPath": { + "default": "", + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.PersistentVolumeClaimSpec": { + "description": "PersistentVolumeClaimSpec describes the common attributes of storage devices and allows a Source for provider-specific attributes", + "properties": { + "accessModes": { + "description": "accessModes contains the desired access modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "dataSource": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.TypedLocalObjectReference" + } + ], + "description": "dataSource field can be used to specify either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) * An existing PVC (PersistentVolumeClaim) If the provisioner or an external controller can support the specified data source, it will create a new volume based on the contents of the specified data source. When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. If the namespace is specified, then dataSourceRef will not be copied to dataSource." + }, + "dataSourceRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.TypedObjectReference" + } + ], + "description": "dataSourceRef specifies the object from which to populate the volume with data, if a non-empty volume is desired. This may be any object from a non-empty API group (non core object) or a PersistentVolumeClaim object. When this field is specified, volume binding will only succeed if the type of the specified object matches some installed volume populator or dynamic provisioner. This field will replace the functionality of the dataSource field and as such if both fields are non-empty, they must have the same value. For backwards compatibility, when namespace isn't specified in dataSourceRef, both fields (dataSource and dataSourceRef) will be set to the same value automatically if one of them is empty and the other is non-empty. When namespace is specified in dataSourceRef, dataSource isn't set to the same value and must be empty. There are three important differences between dataSource and dataSourceRef: * While dataSource only allows two specific types of objects, dataSourceRef\n allows any non-core object, as well as PersistentVolumeClaim objects.\n* While dataSource ignores disallowed values (dropping them), dataSourceRef\n preserves all values, and generates an error if a disallowed value is\n specified.\n* While dataSource only allows local objects, dataSourceRef allows objects\n in any namespaces.\n(Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled." + }, + "resources": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeResourceRequirements" + } + ], + "default": {}, + "description": "resources represents the minimum resources the volume should have. Users are allowed to specify resource requirements that are lower than previous value but must still be higher than capacity recorded in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources" + }, + "selector": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" + } + ], + "description": "selector is a label query over volumes to consider for binding." + }, + "storageClassName": { + "description": "storageClassName is the name of the StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1", + "type": "string" + }, + "volumeAttributesClassName": { + "description": "volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. If specified, the CSI driver will create or update the volume with the attributes defined in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, it can be changed after the claim is created. An empty string or nil value indicates that no VolumeAttributesClass will be applied to the claim. If the claim enters an Infeasible error state, this field can be reset to its previous value (including nil) to cancel the modification. If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource exists. More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/", + "type": "string" + }, + "volumeMode": { + "description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.", + "type": "string" + }, + "volumeName": { + "description": "volumeName is the binding reference to the PersistentVolume backing this claim.", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.PersistentVolumeClaimTemplate": { + "description": "PersistentVolumeClaimTemplate is used to produce PersistentVolumeClaim objects as part of an EphemeralVolumeSource.", + "properties": { + "metadata": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ], + "default": {}, + "description": "May contain labels and annotations that will be copied into the PVC when creating it. No other fields are allowed and will be rejected during validation." + }, + "spec": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimSpec" + } + ], + "default": {}, + "description": "The specification for the PersistentVolumeClaim. The entire content is copied unchanged into the PVC that gets created from this template. The same fields as in a PersistentVolumeClaim are also valid here." + } + }, + "required": [ + "spec" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PersistentVolumeClaimVolumeSource": { + "description": "PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system).", + "properties": { + "claimName": { + "default": "", + "description": "claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", + "type": "string" + }, + "readOnly": { + "description": "readOnly Will force the ReadOnly setting in VolumeMounts. Default false.", + "type": "boolean" + } + }, + "required": [ + "claimName" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PhotonPersistentDiskVolumeSource": { + "description": "Represents a Photon Controller persistent disk resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "pdID": { + "default": "", + "description": "pdID is the ID that identifies Photon Controller persistent disk", + "type": "string" + } + }, + "required": [ + "pdID" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PodAffinity": { + "description": "Pod affinity is a group of inter pod affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.WeightedPodAffinityTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.PodAffinityTerm": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key \u003ctopologyKey\u003e matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" + } + ], + "description": "A label query over a set of resources, in this case pods. If it's null, this PodAffinityTerm matches with no Pods." + }, + "matchLabelKeys": { + "description": "MatchLabelKeys is a set of pod label keys to select which pods will be taken into consideration. The keys are used to lookup values from the incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` to select the group of existing pods which pods will be taken into consideration for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming pod labels will be ignored. The default value is empty. The same key is forbidden to exist in both matchLabelKeys and labelSelector. Also, matchLabelKeys cannot be set when labelSelector isn't set.", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "mismatchLabelKeys": { + "description": "MismatchLabelKeys is a set of pod label keys to select which pods will be taken into consideration. The keys are used to lookup values from the incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` to select the group of existing pods which pods will be taken into consideration for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming pod labels will be ignored. The default value is empty. The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. Also, mismatchLabelKeys cannot be set when labelSelector isn't set.", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "namespaceSelector": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector" + } + ], + "description": "A label query over the set of namespaces that the term applies to. The term is applied to the union of the namespaces selected by this field and the ones listed in the namespaces field. null selector and null or empty namespaces list means \"this pod's namespace\". An empty selector ({}) matches all namespaces." + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "topologyKey": { + "default": "", + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PodAntiAffinity": { + "description": "Pod anti affinity is a group of inter pod anti affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and subtracting \"weight\" from the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.WeightedPodAffinityTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.PodCertificateProjection": { + "description": "PodCertificateProjection provides a private key and X.509 certificate in the pod filesystem.", + "properties": { + "certificateChainPath": { + "description": "Write the certificate chain at this path in the projected volume.\n\nMost applications should use credentialBundlePath. When using keyPath and certificateChainPath, your application needs to check that the key and leaf certificate are consistent, because it is possible to read the files mid-rotation.", + "type": "string" + }, + "credentialBundlePath": { + "description": "Write the credential bundle at this path in the projected volume.\n\nThe credential bundle is a single file that contains multiple PEM blocks. The first PEM block is a PRIVATE KEY block, containing a PKCS#8 private key.\n\nThe remaining blocks are CERTIFICATE blocks, containing the issued certificate chain from the signer (leaf and any intermediates).\n\nUsing credentialBundlePath lets your Pod's application code make a single atomic read that retrieves a consistent key and certificate chain. If you project them to separate files, your application code will need to additionally check that the leaf certificate was issued to the key.", + "type": "string" + }, + "keyPath": { + "description": "Write the key at this path in the projected volume.\n\nMost applications should use credentialBundlePath. When using keyPath and certificateChainPath, your application needs to check that the key and leaf certificate are consistent, because it is possible to read the files mid-rotation.", + "type": "string" + }, + "keyType": { + "description": "The type of keypair Kubelet will generate for the pod.\n\nValid values are \"RSA3072\", \"RSA4096\", \"ECDSAP256\", \"ECDSAP384\", \"ECDSAP521\", and \"ED25519\".", + "type": "string" + }, + "maxExpirationSeconds": { + "description": "maxExpirationSeconds is the maximum lifetime permitted for the certificate.\n\nKubelet copies this value verbatim into the PodCertificateRequests it generates for this projection.\n\nIf omitted, kube-apiserver will set it to 86400(24 hours). kube-apiserver will reject values shorter than 3600 (1 hour). The maximum allowable value is 7862400 (91 days).\n\nThe signer implementation is then free to issue a certificate with any lifetime *shorter* than MaxExpirationSeconds, but no shorter than 3600 seconds (1 hour). This constraint is enforced by kube-apiserver. `kubernetes.io` signers will never issue certificates with a lifetime longer than 24 hours.", + "format": "int32", + "type": "integer" + }, + "signerName": { + "description": "Kubelet's generated CSRs will be addressed to this signer.", + "type": "string" + }, + "userAnnotations": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "description": "userAnnotations allow pod authors to pass additional information to the signer implementation. Kubernetes does not restrict or validate this metadata in any way.\n\nThese values are copied verbatim into the `spec.unverifiedUserAnnotations` field of the PodCertificateRequest objects that Kubelet creates.\n\nEntries are subject to the same validation as object metadata annotations, with the addition that all keys must be domain-prefixed. No restrictions are placed on values, except an overall size limitation on the entire field.\n\nSigners should document the keys and values they support. Signers should deny requests that contain keys they do not recognize.", + "type": "object" + } + }, + "required": [ + "signerName", + "keyType" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PortworxVolumeSource": { + "description": "PortworxVolumeSource represents a Portworx volume resource.", + "properties": { + "fsType": { + "description": "fSType represents the filesystem type to mount Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "volumeID": { + "default": "", + "description": "volumeID uniquely identifies a Portworx volume", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": "object" + }, + "io.k8s.api.core.v1.PreferredSchedulingTerm": { + "description": "An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", + "properties": { + "preference": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NodeSelectorTerm" + } + ], + "default": {}, + "description": "A node selector term, associated with the corresponding weight." + }, + "weight": { + "default": 0, + "description": "Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "preference" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ProjectedVolumeSource": { + "description": "Represents a projected volume source", + "properties": { + "defaultMode": { + "description": "defaultMode are the mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "sources": { + "description": "sources is the list of volume projections. Each entry in this list handles one source.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.VolumeProjection" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.QuobyteVolumeSource": { + "description": "Represents a Quobyte mount that lasts the lifetime of a pod. Quobyte volumes do not support ownership management or SELinux relabeling.", + "properties": { + "group": { + "description": "group to map volume access to Default is no group", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the Quobyte volume to be mounted with read-only permissions. Defaults to false.", + "type": "boolean" + }, + "registry": { + "default": "", + "description": "registry represents a single or multiple Quobyte Registry services specified as a string as host:port pair (multiple entries are separated with commas) which acts as the central registry for volumes", + "type": "string" + }, + "tenant": { + "description": "tenant owning the given Quobyte volume in the Backend Used with dynamically provisioned Quobyte volumes, value is set by the plugin", + "type": "string" + }, + "user": { + "description": "user to map volume access to Defaults to serivceaccount user", + "type": "string" + }, + "volume": { + "default": "", + "description": "volume is a string that references an already created Quobyte volume by name.", + "type": "string" + } + }, + "required": [ + "registry", + "volume" + ], + "type": "object" + }, + "io.k8s.api.core.v1.RBDVolumeSource": { + "description": "Represents a Rados Block Device mount that lasts the lifetime of a pod. RBD volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd", + "type": "string" + }, + "image": { + "default": "", + "description": "image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + }, + "keyring": { + "default": "/etc/ceph/keyring", + "description": "keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + }, + "monitors": { + "description": "monitors is a collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "pool": { + "default": "rbd", + "description": "pool is the rados pool name. Default is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef is name of the authentication secret for RBDUser. If provided overrides keyring. Default is nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it" + }, + "user": { + "default": "admin", + "description": "user is the rados user name. Default is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + } + }, + "required": [ + "monitors", + "image" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ResourceClaim": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "default": "", + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + }, + "request": { + "description": "Request is the name chosen for a request in the referenced claim. If empty, everything from the claim is made available, otherwise only the result of this request.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "io.k8s.api.core.v1.ResourceFieldSelector": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": "string" + }, + "divisor": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + } + ], + "description": "Specifies the output format of the exposed resources, defaults to \"1\"" + }, + "resource": { + "default": "", + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.ResourceRequirements": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis field depends on the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ResourceClaim" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.ScaleIOVolumeSource": { + "description": "ScaleIOVolumeSource represents a persistent ScaleIO volume", + "properties": { + "fsType": { + "default": "xfs", + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Default is \"xfs\".", + "type": "string" + }, + "gateway": { + "default": "", + "description": "gateway is the host address of the ScaleIO API Gateway.", + "type": "string" + }, + "protectionDomain": { + "description": "protectionDomain is the name of the ScaleIO Protection Domain for the configured storage.", + "type": "string" + }, + "readOnly": { + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef references to the secret for ScaleIO user and other sensitive information. If this is not provided, Login operation will fail." + }, + "sslEnabled": { + "description": "sslEnabled Flag enable/disable SSL communication with Gateway, default false", + "type": "boolean" + }, + "storageMode": { + "default": "ThinProvisioned", + "description": "storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned.", + "type": "string" + }, + "storagePool": { + "description": "storagePool is the ScaleIO Storage Pool associated with the protection domain.", + "type": "string" + }, + "system": { + "default": "", + "description": "system is the name of the storage system as configured in ScaleIO.", + "type": "string" + }, + "volumeName": { + "description": "volumeName is the name of a volume already created in the ScaleIO system that is associated with this volume source.", + "type": "string" + } + }, + "required": [ + "gateway", + "system", + "secretRef" + ], + "type": "object" + }, + "io.k8s.api.core.v1.SecretEnvSource": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": "boolean" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.SecretKeySelector": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "default": "", + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": "boolean" + } + }, + "required": [ + "key" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.SecretProjection": { + "description": "Adapts a secret into a projected volume.\n\nThe contents of the target Secret's Data field will be presented in a projected volume as files using the keys in the Data field as the file names. Note that this is identical to a secret volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "name": { + "default": "", + "description": "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "optional": { + "description": "optional field specify whether the Secret or its key must be defined", + "type": "boolean" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.SecretVolumeSource": { + "description": "Adapts a Secret into a volume.\n\nThe contents of the target Secret's Data field will be presented in a volume as files using the keys in the Data field as the file names. Secret volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": "integer" + }, + "items": { + "description": "items If unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.KeyToPath" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "optional": { + "description": "optional field specify whether the Secret or its keys must be defined", + "type": "boolean" + }, + "secretName": { + "description": "secretName is the name of the secret in the pod's namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.ServiceAccountTokenProjection": { + "description": "ServiceAccountTokenProjection represents a projected service account token volume. This projection can be used to insert a service account token into the pods runtime filesystem for use against APIs (Kubernetes API Server or otherwise).", + "properties": { + "audience": { + "description": "audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.", + "type": "string" + }, + "expirationSeconds": { + "description": "expirationSeconds is the requested duration of validity of the service account token. As the token approaches expiration, the kubelet volume plugin will proactively rotate the service account token. The kubelet will start trying to rotate the token if the token is older than 80 percent of its time to live or if the token is older than 24 hours.Defaults to 1 hour and must be at least 10 minutes.", + "format": "int64", + "type": "integer" + }, + "path": { + "default": "", + "description": "path is the path relative to the mount point of the file to project the token into.", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "io.k8s.api.core.v1.StorageOSVolumeSource": { + "description": "Represents a StorageOS persistent volume resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": "boolean" + }, + "secretRef": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ], + "description": "secretRef specifies the secret to use for obtaining the StorageOS API credentials. If not specified, default values will be attempted." + }, + "volumeName": { + "description": "volumeName is the human-readable name of the StorageOS volume. Volume names are only unique within a namespace.", + "type": "string" + }, + "volumeNamespace": { + "description": "volumeNamespace specifies the scope of the volume within StorageOS. If no namespace is specified then the Pod's namespace will be used. This allows the Kubernetes name scoping to be mirrored within StorageOS for tighter integration. Set VolumeName to any name to override the default behaviour. Set to \"default\" if you are not using namespaces within StorageOS. Namespaces that do not pre-exist within StorageOS will be created.", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.Toleration": { + "description": "The pod this Toleration is attached to tolerates any taint that matches the triple \u003ckey,value,effect\u003e using the matching operator \u003coperator\u003e.", + "properties": { + "effect": { + "description": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.", + "type": "string" + }, + "key": { + "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.", + "type": "string" + }, + "operator": { + "description": "Operator represents a key's relationship to the value. Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category. Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators).", + "type": "string" + }, + "tolerationSeconds": { + "description": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.", + "format": "int64", + "type": "integer" + }, + "value": { + "description": "Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.TypedLocalObjectReference": { + "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": "string" + }, + "kind": { + "default": "", + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "default": "", + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.api.core.v1.TypedObjectReference": { + "description": "TypedObjectReference contains enough information to let you locate the typed referenced object", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": "string" + }, + "kind": { + "default": "", + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "default": "", + "description": "Name is the name of resource being referenced", + "type": "string" + }, + "namespace": { + "description": "Namespace is the namespace of resource being referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + }, + "io.k8s.api.core.v1.Volume": { + "description": "Volume represents a named volume in a pod that may be accessed by any container in the pod.", + "properties": { + "awsElasticBlockStore": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.AWSElasticBlockStoreVolumeSource" + } + ], + "description": "awsElasticBlockStore represents an AWS Disk resource that is attached to a kubelet's host machine and then exposed to the pod. Deprecated: AWSElasticBlockStore is deprecated. All operations for the in-tree awsElasticBlockStore type are redirected to the ebs.csi.aws.com CSI driver. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore" + }, + "azureDisk": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.AzureDiskVolumeSource" + } + ], + "description": "azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod. Deprecated: AzureDisk is deprecated. All operations for the in-tree azureDisk type are redirected to the disk.csi.azure.com CSI driver." + }, + "azureFile": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.AzureFileVolumeSource" + } + ], + "description": "azureFile represents an Azure File Service mount on the host and bind mount to the pod. Deprecated: AzureFile is deprecated. All operations for the in-tree azureFile type are redirected to the file.csi.azure.com CSI driver." + }, + "cephfs": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.CephFSVolumeSource" + } + ], + "description": "cephFS represents a Ceph FS mount on the host that shares a pod's lifetime. Deprecated: CephFS is deprecated and the in-tree cephfs type is no longer supported." + }, + "cinder": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.CinderVolumeSource" + } + ], + "description": "cinder represents a cinder volume attached and mounted on kubelets host machine. Deprecated: Cinder is deprecated. All operations for the in-tree cinder type are redirected to the cinder.csi.openstack.org CSI driver. More info: https://examples.k8s.io/mysql-cinder-pd/README.md" + }, + "configMap": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapVolumeSource" + } + ], + "description": "configMap represents a configMap that should populate this volume" + }, + "csi": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.CSIVolumeSource" + } + ], + "description": "csi (Container Storage Interface) represents ephemeral storage that is handled by certain external CSI drivers." + }, + "downwardAPI": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIVolumeSource" + } + ], + "description": "downwardAPI represents downward API about the pod that should populate this volume" + }, + "emptyDir": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.EmptyDirVolumeSource" + } + ], + "description": "emptyDir represents a temporary directory that shares a pod's lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir" + }, + "ephemeral": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.EphemeralVolumeSource" + } + ], + "description": "ephemeral represents a volume that is handled by a cluster storage driver. The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, and deleted when the pod is removed.\n\nUse this if: a) the volume is only needed while the pod runs, b) features of normal volumes like restoring from snapshot or capacity\n tracking are needed,\nc) the storage driver is specified through a storage class, and d) the storage driver supports dynamic volume provisioning through\n a PersistentVolumeClaim (see EphemeralVolumeSource for more\n information on the connection between this volume type\n and PersistentVolumeClaim).\n\nUse PersistentVolumeClaim or one of the vendor-specific APIs for volumes that persist for longer than the lifecycle of an individual pod.\n\nUse CSI for light-weight local ephemeral volumes if the CSI driver is meant to be used that way - see the documentation of the driver for more information.\n\nA pod can use both types of ephemeral volumes and persistent volumes at the same time." + }, + "fc": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.FCVolumeSource" + } + ], + "description": "fc represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod." + }, + "flexVolume": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.FlexVolumeSource" + } + ], + "description": "flexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin. Deprecated: FlexVolume is deprecated. Consider using a CSIDriver instead." + }, + "flocker": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.FlockerVolumeSource" + } + ], + "description": "flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running. Deprecated: Flocker is deprecated and the in-tree flocker type is no longer supported." + }, + "gcePersistentDisk": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.GCEPersistentDiskVolumeSource" + } + ], + "description": "gcePersistentDisk represents a GCE Disk resource that is attached to a kubelet's host machine and then exposed to the pod. Deprecated: GCEPersistentDisk is deprecated. All operations for the in-tree gcePersistentDisk type are redirected to the pd.csi.storage.gke.io CSI driver. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk" + }, + "gitRepo": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.GitRepoVolumeSource" + } + ], + "description": "gitRepo represents a git repository at a particular revision. Deprecated: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container." + }, + "glusterfs": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.GlusterfsVolumeSource" + } + ], + "description": "glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. Deprecated: Glusterfs is deprecated and the in-tree glusterfs type is no longer supported." + }, + "hostPath": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.HostPathVolumeSource" + } + ], + "description": "hostPath represents a pre-existing file or directory on the host machine that is directly exposed to the container. This is generally used for system agents or other privileged things that are allowed to see the host machine. Most containers will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath" + }, + "image": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ImageVolumeSource" + } + ], + "description": "image represents an OCI object (a container image or artifact) pulled and mounted on the kubelet's host machine. The volume is resolved at pod startup depending on which PullPolicy value is provided:\n\n- Always: the kubelet always attempts to pull the reference. Container creation will fail If the pull fails. - Never: the kubelet never pulls the reference and only uses a local image or artifact. Container creation will fail if the reference isn't present. - IfNotPresent: the kubelet pulls if the reference isn't already present on disk. Container creation will fail if the reference isn't present and the pull fails.\n\nThe volume gets re-resolved if the pod gets deleted and recreated, which means that new remote content will become available on pod recreation. A failure to resolve or pull the image during pod startup will block containers from starting and may add significant latency. Failures will be retried using normal volume backoff and will be reported on the pod reason and message. The types of objects that may be mounted by this volume are defined by the container runtime implementation on a host machine and at minimum must include all valid types supported by the container image field. The OCI object gets mounted in a single directory (spec.containers[*].volumeMounts.mountPath) by merging the manifest layers in the same way as for container images. The volume will be mounted read-only (ro) and non-executable files (noexec). Sub path mounts for containers are not supported (spec.containers[*].volumeMounts.subpath) before 1.33. The field spec.securityContext.fsGroupChangePolicy has no effect on this volume type." + }, + "iscsi": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ISCSIVolumeSource" + } + ], + "description": "iscsi represents an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes/#iscsi" + }, + "name": { + "default": "", + "description": "name of the volume. Must be a DNS_LABEL and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "nfs": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.NFSVolumeSource" + } + ], + "description": "nfs represents an NFS mount on the host that shares a pod's lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs" + }, + "persistentVolumeClaim": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PersistentVolumeClaimVolumeSource" + } + ], + "description": "persistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims" + }, + "photonPersistentDisk": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PhotonPersistentDiskVolumeSource" + } + ], + "description": "photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host machine. Deprecated: PhotonPersistentDisk is deprecated and the in-tree photonPersistentDisk type is no longer supported." + }, + "portworxVolume": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PortworxVolumeSource" + } + ], + "description": "portworxVolume represents a portworx volume attached and mounted on kubelets host machine. Deprecated: PortworxVolume is deprecated. All operations for the in-tree portworxVolume type are redirected to the pxd.portworx.com CSI driver when the CSIMigrationPortworx feature-gate is on." + }, + "projected": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ProjectedVolumeSource" + } + ], + "description": "projected items for all in one resources secrets, configmaps, and downward API" + }, + "quobyte": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.QuobyteVolumeSource" + } + ], + "description": "quobyte represents a Quobyte mount on the host that shares a pod's lifetime. Deprecated: Quobyte is deprecated and the in-tree quobyte type is no longer supported." + }, + "rbd": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.RBDVolumeSource" + } + ], + "description": "rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. Deprecated: RBD is deprecated and the in-tree rbd type is no longer supported." + }, + "scaleIO": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ScaleIOVolumeSource" + } + ], + "description": "scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes. Deprecated: ScaleIO is deprecated and the in-tree scaleIO type is no longer supported." + }, + "secret": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretVolumeSource" + } + ], + "description": "secret represents a secret that should populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret" + }, + "storageos": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.StorageOSVolumeSource" + } + ], + "description": "storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes. Deprecated: StorageOS is deprecated and the in-tree storageos type is no longer supported." + }, + "vsphereVolume": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.VsphereVirtualDiskVolumeSource" + } + ], + "description": "vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine. Deprecated: VsphereVolume is deprecated. All operations for the in-tree vsphereVolume type are redirected to the csi.vsphere.vmware.com CSI driver." + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "io.k8s.api.core.v1.VolumeMount": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "default": "", + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10. When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified (which defaults to None).", + "type": "string" + }, + "name": { + "default": "", + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": "boolean" + }, + "recursiveReadOnly": { + "description": "RecursiveReadOnly specifies whether read-only mounts should be handled recursively.\n\nIf ReadOnly is false, this field has no meaning and must be unspecified.\n\nIf ReadOnly is true, and this field is set to Disabled, the mount is not made recursively read-only. If this field is set to IfPossible, the mount is made recursively read-only, if it is supported by the container runtime. If this field is set to Enabled, the mount is made recursively read-only if it is supported by the container runtime, otherwise the pod will not be started and an error will be generated to indicate the reason.\n\nIf this field is set to IfPossible or Enabled, MountPropagation must be set to None (or be unspecified, which defaults to None).\n\nIf this field is not specified, it is treated as an equivalent of Disabled.", + "type": "string" + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": "string" + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": "string" + } + }, + "required": [ + "name", + "mountPath" + ], + "type": "object" + }, + "io.k8s.api.core.v1.VolumeProjection": { + "description": "Projection that may be projected along with other supported volume types. Exactly one of these fields must be set.", + "properties": { + "clusterTrustBundle": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ClusterTrustBundleProjection" + } + ], + "description": "ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field of ClusterTrustBundle objects in an auto-updating file.\n\nAlpha, gated by the ClusterTrustBundleProjection feature gate.\n\nClusterTrustBundle objects can either be selected by name, or by the combination of signer name and a label selector.\n\nKubelet performs aggressive normalization of the PEM contents written into the pod filesystem. Esoteric PEM features such as inter-block comments and block headers are stripped. Certificates are deduplicated. The ordering of certificates within the file is arbitrary, and Kubelet may change the order over time." + }, + "configMap": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ConfigMapProjection" + } + ], + "description": "configMap information about the configMap data to project" + }, + "downwardAPI": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.DownwardAPIProjection" + } + ], + "description": "downwardAPI information about the downwardAPI data to project" + }, + "podCertificate": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodCertificateProjection" + } + ], + "description": "Projects an auto-rotating credential bundle (private key and certificate chain) that the pod can use either as a TLS client or server.\n\nKubelet generates a private key and uses it to send a PodCertificateRequest to the named signer. Once the signer approves the request and issues a certificate chain, Kubelet writes the key and certificate chain to the pod filesystem. The pod does not start until certificates have been issued for each podCertificate projected volume source in its spec.\n\nKubelet will begin trying to rotate the certificate at the time indicated by the signer using the PodCertificateRequest.Status.BeginRefreshAt timestamp.\n\nKubelet can write a single file, indicated by the credentialBundlePath field, or separate files, indicated by the keyPath and certificateChainPath fields.\n\nThe credential bundle is a single file in PEM format. The first PEM entry is the private key (in PKCS#8 format), and the remaining PEM entries are the certificate chain issued by the signer (typically, signers will return their certificate chain in leaf-to-root order).\n\nPrefer using the credential bundle format, since your application code can read it atomically. If you use keyPath and certificateChainPath, your application must make two separate file reads. If these coincide with a certificate rotation, it is possible that the private key and leaf certificate you read may not correspond to each other. Your application will need to check for this condition, and re-read until they are consistent.\n\nThe named signer controls chooses the format of the certificate it issues; consult the signer implementation's documentation to learn how to use the certificates it issues." + }, + "secret": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.SecretProjection" + } + ], + "description": "secret information about the secret data to project" + }, + "serviceAccountToken": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.ServiceAccountTokenProjection" + } + ], + "description": "serviceAccountToken is information about the serviceAccountToken data to project" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.VolumeResourceRequirements": { + "description": "VolumeResourceRequirements describes the storage resource requirements for a volume.", + "properties": { + "limits": { + "additionalProperties": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.api.resource.Quantity" + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + }, + "io.k8s.api.core.v1.VsphereVirtualDiskVolumeSource": { + "description": "Represents a vSphere volume resource.", + "properties": { + "fsType": { + "description": "fsType is filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": "string" + }, + "storagePolicyID": { + "description": "storagePolicyID is the storage Policy Based Management (SPBM) profile ID associated with the StoragePolicyName.", + "type": "string" + }, + "storagePolicyName": { + "description": "storagePolicyName is the storage Policy Based Management (SPBM) profile name.", + "type": "string" + }, + "volumePath": { + "default": "", + "description": "volumePath is the path that identifies vSphere volume vmdk", + "type": "string" + } + }, + "required": [ + "volumePath" + ], + "type": "object" + }, + "io.k8s.api.core.v1.WeightedPodAffinityTerm": { + "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", + "properties": { + "podAffinityTerm": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.PodAffinityTerm" + } + ], + "default": {}, + "description": "Required. A pod affinity term, associated with the corresponding weight." + }, + "weight": { + "default": 0, + "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "podAffinityTerm" + ], + "type": "object" + }, + "io.k8s.apimachinery.pkg.api.resource.Quantity": { + "description": "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` \u003cquantity\u003e ::= \u003csignedNumber\u003e\u003csuffix\u003e\n\n\t(Note that \u003csuffix\u003e may be empty, from the \"\" case in \u003cdecimalSI\u003e.)\n\n\u003cdigit\u003e ::= 0 | 1 | ... | 9 \u003cdigits\u003e ::= \u003cdigit\u003e | \u003cdigit\u003e\u003cdigits\u003e \u003cnumber\u003e ::= \u003cdigits\u003e | \u003cdigits\u003e.\u003cdigits\u003e | \u003cdigits\u003e. | .\u003cdigits\u003e \u003csign\u003e ::= \"+\" | \"-\" \u003csignedNumber\u003e ::= \u003cnumber\u003e | \u003csign\u003e\u003cnumber\u003e \u003csuffix\u003e ::= \u003cbinarySI\u003e | \u003cdecimalExponent\u003e | \u003cdecimalSI\u003e \u003cbinarySI\u003e ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n\u003cdecimalSI\u003e ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n\u003cdecimalExponent\u003e ::= \"e\" \u003csignedNumber\u003e | \"E\" \u003csignedNumber\u003e ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelectorRequirement" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "matchLabels": { + "additionalProperties": { + "default": "", + "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" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelectorRequirement": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "default": "", + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "default": "", + "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": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "required": [ + "key", + "operator" + ], + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ], + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type." + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ], + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over." + } + }, + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object" + }, + "creationTimestamp": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ], + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata" + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": "integer" + }, + "deletionTimestamp": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ], + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata" + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "default": "", + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": "integer" + }, + "labels": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object" + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ], + "default": {} + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "default": "", + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "default": "", + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "default": "", + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "default": "", + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": "string" + } + } + } +} \ No newline at end of file diff --git a/internal/operator-controller/rukpak/bundle/source/source.go b/internal/operator-controller/rukpak/bundle/source/source.go index 0f5d3f185c..49ed0c32bf 100644 --- a/internal/operator-controller/rukpak/bundle/source/source.go +++ b/internal/operator-controller/rukpak/bundle/source/source.go @@ -20,6 +20,10 @@ import ( registry "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/operator-registry" ) +const ( + PropertyOLMProperties = "olm.properties" +) + type BundleSource interface { GetBundle() (bundle.RegistryV1, error) } @@ -173,6 +177,9 @@ func copyMetadataPropertiesToCSV(csv *v1alpha1.ClusterServiceVersion, fsys fs.FS if err != nil { return fmt.Errorf("failed to marshal registry+v1 properties to json: %w", err) } - csv.Annotations["olm.properties"] = string(allPropertiesJSON) + if csv.Annotations == nil { + csv.Annotations = map[string]string{} + } + csv.Annotations[PropertyOLMProperties] = string(allPropertiesJSON) return nil } diff --git a/internal/operator-controller/rukpak/render/registryv1/generators/generators.go b/internal/operator-controller/rukpak/render/registryv1/generators/generators.go index 7d5d435ead..8f45bb7620 100644 --- a/internal/operator-controller/rukpak/render/registryv1/generators/generators.go +++ b/internal/operator-controller/rukpak/render/registryv1/generators/generators.go @@ -3,6 +3,7 @@ package generators import ( "cmp" "fmt" + "reflect" "slices" "strconv" "strings" @@ -21,6 +22,7 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle" + "github.com/operator-framework/operator-controller/internal/operator-controller/config" "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" @@ -98,6 +100,9 @@ func BundleCSVDeploymentGenerator(rv1 *bundle.RegistryV1, opts render.Options) ( ensureCorrectDeploymentCertVolumes(deploymentResource, *secretInfo) } + // Apply deployment configuration if provided + applyCustomConfigToDeployment(deploymentResource, opts.DeploymentConfig) + objs = append(objs, deploymentResource) } return objs, nil @@ -578,3 +583,214 @@ func getWebhookNamespaceSelector(targetNamespaces []string) *metav1.LabelSelecto } return nil } + +// applyCustomConfigToDeployment applies the deployment configuration to all containers in the deployment. +// It follows OLMv0 behavior for applying configuration to deployments. +// See https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go +func applyCustomConfigToDeployment(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if config == nil { + return + } + + // Apply all configuration modifications following OLMv0 behavior + applyEnvironmentConfig(deployment, config) + applyEnvironmentFromConfig(deployment, config) + applyVolumeConfig(deployment, config) + applyVolumeMountConfig(deployment, config) + applyTolerationsConfig(deployment, config) + applyResourcesConfig(deployment, config) + applyNodeSelectorConfig(deployment, config) + applyAffinityConfig(deployment, config) + applyAnnotationsConfig(deployment, config) +} + +// applyEnvironmentConfig applies environment variables to all containers in the deployment. +// Environment variables from config override existing environment variables with the same name. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L11-L27 +func applyEnvironmentConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.Env) == 0 { + return + } + + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + + // Create a map to track existing env var names for override behavior + existingEnvMap := make(map[string]int) + for idx, env := range container.Env { + existingEnvMap[env.Name] = idx + } + + // Apply config env vars, overriding existing ones with same name + for _, configEnv := range config.Env { + if existingIdx, exists := existingEnvMap[configEnv.Name]; exists { + // Override existing env var + container.Env[existingIdx] = configEnv + } else { + // Append new env var + container.Env = append(container.Env, configEnv) + } + } + } +} + +// applyEnvironmentFromConfig appends EnvFrom sources to all containers in the deployment. +// Duplicate EnvFrom sources are not added. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L65-L81 +func applyEnvironmentFromConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.EnvFrom) == 0 { + return + } + + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + + // Check for duplicates before appending + for _, configEnvFrom := range config.EnvFrom { + isDuplicate := false + for _, existingEnvFrom := range container.EnvFrom { + if reflect.DeepEqual(existingEnvFrom, configEnvFrom) { + isDuplicate = true + break + } + } + if !isDuplicate { + container.EnvFrom = append(container.EnvFrom, configEnvFrom) + } + } + } +} + +// applyVolumeConfig appends volumes to the deployment's pod spec. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L104-L117 +func applyVolumeConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.Volumes) == 0 { + return + } + + deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, config.Volumes...) +} + +// applyVolumeMountConfig appends volume mounts to all containers in the deployment. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L149-L165 +func applyVolumeMountConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.VolumeMounts) == 0 { + return + } + + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + container.VolumeMounts = append(container.VolumeMounts, config.VolumeMounts...) + } +} + +// applyTolerationsConfig appends tolerations to the deployment's pod spec. +// Duplicate tolerations are not added. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L197-L209 +func applyTolerationsConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.Tolerations) == 0 { + return + } + + // Check for duplicates before appending + for _, configToleration := range config.Tolerations { + isDuplicate := false + for _, existingToleration := range deployment.Spec.Template.Spec.Tolerations { + if reflect.DeepEqual(existingToleration, configToleration) { + isDuplicate = true + break + } + } + if !isDuplicate { + deployment.Spec.Template.Spec.Tolerations = append(deployment.Spec.Template.Spec.Tolerations, configToleration) + } + } +} + +// applyResourcesConfig applies resource requirements to all containers in the deployment. +// This completely replaces existing resource requirements. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L236-L255 +func applyResourcesConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if config.Resources == nil { + return + } + + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + container.Resources = *config.Resources + } +} + +// applyNodeSelectorConfig applies node selector to the deployment's pod spec. +// This completely replaces existing node selector. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L257-L271 +func applyNodeSelectorConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if config.NodeSelector == nil { + return + } + + deployment.Spec.Template.Spec.NodeSelector = config.NodeSelector +} + +// applyAffinityConfig applies affinity configuration to the deployment's pod spec. +// This selectively overrides non-nil affinity sub-attributes. +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L273-L341 +func applyAffinityConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if config.Affinity == nil { + return + } + + if deployment.Spec.Template.Spec.Affinity == nil { + deployment.Spec.Template.Spec.Affinity = &corev1.Affinity{} + } + + if config.Affinity.NodeAffinity != nil { + deployment.Spec.Template.Spec.Affinity.NodeAffinity = config.Affinity.NodeAffinity + } + + if config.Affinity.PodAffinity != nil { + deployment.Spec.Template.Spec.Affinity.PodAffinity = config.Affinity.PodAffinity + } + + if config.Affinity.PodAntiAffinity != nil { + deployment.Spec.Template.Spec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity + } +} + +// applyAnnotationsConfig applies annotations to the deployment and its pod template. +// Existing deployment and pod annotations take precedence over config annotations (no override). +// This follows OLMv0 behavior: +// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L343-L378 +func applyAnnotationsConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) { + if len(config.Annotations) == 0 { + return + } + + // Apply to deployment metadata + if deployment.Annotations == nil { + deployment.Annotations = make(map[string]string) + } + for key, value := range config.Annotations { + if _, exists := deployment.Annotations[key]; !exists { + deployment.Annotations[key] = value + } + } + + // Apply to pod template metadata + if deployment.Spec.Template.Annotations == nil { + deployment.Spec.Template.Annotations = make(map[string]string) + } + for key, value := range config.Annotations { + if _, exists := deployment.Spec.Template.Annotations[key]; !exists { + deployment.Spec.Template.Annotations[key] = value + } + } +} 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 59be3c6df1..22ce6d28be 100644 --- a/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go +++ b/internal/operator-controller/rukpak/render/registryv1/generators/generators_test.go @@ -12,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" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/intstr" @@ -20,6 +21,7 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-controller/internal/operator-controller/config" "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/generators" @@ -2508,3 +2510,586 @@ func Test_CertProviderResourceGenerator_Succeeds(t *testing.T) { }), }, objs) } + +func Test_BundleCSVDeploymentGenerator_WithDeploymentConfig(t *testing.T) { + for _, tc := range []struct { + name string + bundle *bundle.RegistryV1 + opts render.Options + verify func(*testing.T, []client.Object) + }{ + { + name: "applies env vars from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "manager", + Env: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "existing_value"}, + }, + }, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Env: []corev1.EnvVar{ + {Name: "NEW_VAR", Value: "new_value"}, + {Name: "EXISTING_VAR", Value: "overridden_value"}, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + envVars := dep.Spec.Template.Spec.Containers[0].Env + + // Should have both vars + require.Len(t, envVars, 2) + + // Existing var should be overridden + var existingVar *corev1.EnvVar + for i := range envVars { + if envVars[i].Name == "EXISTING_VAR" { + existingVar = &envVars[i] + break + } + } + require.NotNil(t, existingVar) + require.Equal(t, "overridden_value", existingVar.Value) + + // New var should be added + var newVar *corev1.EnvVar + for i := range envVars { + if envVars[i].Name == "NEW_VAR" { + newVar = &envVars[i] + break + } + } + require.NotNil(t, newVar) + require.Equal(t, "new_value", newVar.Value) + }, + }, + { + name: "applies resources from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + resources := dep.Spec.Template.Spec.Containers[0].Resources + + require.Equal(t, resource.MustParse("100m"), *resources.Requests.Cpu()) + require.Equal(t, resource.MustParse("128Mi"), *resources.Requests.Memory()) + require.Equal(t, resource.MustParse("200m"), *resources.Limits.Cpu()) + require.Equal(t, resource.MustParse("256Mi"), *resources.Limits.Memory()) + }, + }, + { + name: "applies tolerations from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Tolerations: []corev1.Toleration{ + { + Key: "node.kubernetes.io/disk-type", + Operator: corev1.TolerationOpEqual, + Value: "ssd", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + tolerations := dep.Spec.Template.Spec.Tolerations + + require.Len(t, tolerations, 1) + require.Equal(t, "node.kubernetes.io/disk-type", tolerations[0].Key) + require.Equal(t, corev1.TolerationOpEqual, tolerations[0].Operator) + require.Equal(t, "ssd", tolerations[0].Value) + require.Equal(t, corev1.TaintEffectNoSchedule, tolerations[0].Effect) + }, + }, + { + name: "applies node selector from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + NodeSelector: map[string]string{ + "existing-key": "existing-value", + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + NodeSelector: map[string]string{ + "disk-type": "ssd", + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // Node selector should be replaced, not merged + require.Equal(t, map[string]string{"disk-type": "ssd"}, dep.Spec.Template.Spec.NodeSelector) + }, + }, + { + name: "applies affinity from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/arch", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd64", "arm64"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + require.NotNil(t, dep.Spec.Template.Spec.Affinity) + require.NotNil(t, dep.Spec.Template.Spec.Affinity.NodeAffinity) + require.NotNil(t, dep.Spec.Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + require.Len(t, dep.Spec.Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, 1) + }, + }, + { + name: "applies annotations from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithAnnotations(map[string]string{ + "csv-annotation": "csv-value", + }). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "existing-pod-annotation": "existing-pod-value", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Annotations: map[string]string{ + "config-annotation": "config-value", + "existing-pod-annotation": "should-not-override", + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // Deployment annotations should include config annotations + // (CSV annotations are only merged into pod template by the generator) + require.Contains(t, dep.Annotations, "config-annotation") + require.Equal(t, "config-value", dep.Annotations["config-annotation"]) + + // Pod template annotations should include CSV annotations (merged by generator) + // and existing pod annotations should take precedence over config + require.Contains(t, dep.Spec.Template.Annotations, "csv-annotation") + require.Equal(t, "csv-value", dep.Spec.Template.Annotations["csv-annotation"]) + require.Contains(t, dep.Spec.Template.Annotations, "existing-pod-annotation") + require.Equal(t, "existing-pod-value", dep.Spec.Template.Annotations["existing-pod-annotation"]) + require.Contains(t, dep.Spec.Template.Annotations, "config-annotation") + require.Equal(t, "config-value", dep.Spec.Template.Annotations["config-annotation"]) + }, + }, + { + name: "applies volumes and volume mounts from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Volumes: []corev1.Volume{ + { + Name: "config-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "my-config"}, + }, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config-volume", + MountPath: "/etc/config", + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // Check volume was added + require.Len(t, dep.Spec.Template.Spec.Volumes, 1) + require.Equal(t, "config-volume", dep.Spec.Template.Spec.Volumes[0].Name) + + // Check volume mount was added to container + require.Len(t, dep.Spec.Template.Spec.Containers[0].VolumeMounts, 1) + require.Equal(t, "config-volume", dep.Spec.Template.Spec.Containers[0].VolumeMounts[0].Name) + require.Equal(t, "/etc/config", dep.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) + }, + }, + { + name: "applies envFrom from deployment config", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "env-config"}, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "env-secret"}, + }, + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + envFrom := dep.Spec.Template.Spec.Containers[0].EnvFrom + require.Len(t, envFrom, 2) + + // Check ConfigMap ref + require.NotNil(t, envFrom[0].ConfigMapRef) + require.Equal(t, "env-config", envFrom[0].ConfigMapRef.Name) + + // Check Secret ref + require.NotNil(t, envFrom[1].SecretRef) + require.Equal(t, "env-secret", envFrom[1].SecretRef.Name) + }, + }, + { + name: "applies all config fields together", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "manager"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Env: []corev1.EnvVar{ + {Name: "ENV_VAR", Value: "value"}, + }, + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + Tolerations: []corev1.Toleration{ + {Key: "key1", Operator: corev1.TolerationOpEqual, Value: "value1"}, + }, + NodeSelector: map[string]string{ + "disk": "ssd", + }, + Annotations: map[string]string{ + "annotation-key": "annotation-value", + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // Verify env was applied + require.Len(t, dep.Spec.Template.Spec.Containers[0].Env, 1) + require.Equal(t, "ENV_VAR", dep.Spec.Template.Spec.Containers[0].Env[0].Name) + + // Verify resources were applied + require.NotNil(t, dep.Spec.Template.Spec.Containers[0].Resources.Requests) + + // Verify tolerations were applied + require.Len(t, dep.Spec.Template.Spec.Tolerations, 1) + + // Verify node selector was applied + require.Equal(t, map[string]string{"disk": "ssd"}, dep.Spec.Template.Spec.NodeSelector) + + // Verify annotations were applied + require.Contains(t, dep.Annotations, "annotation-key") + require.Contains(t, dep.Spec.Template.Annotations, "annotation-key") + }, + }, + { + name: "applies config to multiple containers", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "container1"}, + {Name: "container2"}, + {Name: "container3"}, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: &config.DeploymentConfig{ + Env: []corev1.EnvVar{ + {Name: "SHARED_VAR", Value: "shared_value"}, + }, + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // All containers should have the env var + for i := range dep.Spec.Template.Spec.Containers { + container := dep.Spec.Template.Spec.Containers[i] + require.Len(t, container.Env, 1) + require.Equal(t, "SHARED_VAR", container.Env[0].Name) + require.Equal(t, "shared_value", container.Env[0].Value) + + // All containers should have the resources + require.NotNil(t, container.Resources.Requests) + require.Equal(t, resource.MustParse("100m"), *container.Resources.Requests.Cpu()) + } + }, + }, + { + name: "nil deployment config does nothing", + bundle: &bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "test-deployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "manager", + Env: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "existing_value"}, + }, + }, + }, + }, + }, + }, + }, + ).Build(), + }, + opts: render.Options{ + InstallNamespace: "test-ns", + TargetNamespaces: []string{"test-ns"}, + DeploymentConfig: nil, + }, + verify: func(t *testing.T, objs []client.Object) { + require.Len(t, objs, 1) + dep := objs[0].(*appsv1.Deployment) + + // Should only have the existing env var + require.Len(t, dep.Spec.Template.Spec.Containers[0].Env, 1) + require.Equal(t, "EXISTING_VAR", dep.Spec.Template.Spec.Containers[0].Env[0].Name) + require.Equal(t, "existing_value", dep.Spec.Template.Spec.Containers[0].Env[0].Value) + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := generators.BundleCSVDeploymentGenerator(tc.bundle, tc.opts) + require.NoError(t, err) + tc.verify(t, objs) + }) + } +} diff --git a/internal/operator-controller/rukpak/render/registryv1/validators/validator.go b/internal/operator-controller/rukpak/render/registryv1/validators/validator.go index 60978aa833..65a483728b 100644 --- a/internal/operator-controller/rukpak/render/registryv1/validators/validator.go +++ b/internal/operator-controller/rukpak/render/registryv1/validators/validator.go @@ -146,7 +146,14 @@ func CheckWebhookDeploymentReferentialIntegrity(rv1 *bundle.RegistryV1) []error delete(webhooksByDeployment, depl.Name) } - var errs []error + totalWebhooks := 0 + for _, webhookDefns := range webhooksByDeployment { + totalWebhooks += len(webhookDefns) + } + if totalWebhooks == 0 { + return nil + } + errs := make([]error, 0, totalWebhooks) // Loop through sorted keys to keep error messages ordered by deployment name for _, deploymentName := range slices.Sorted(maps.Keys(webhooksByDeployment)) { webhookDefns := webhooksByDeployment[deploymentName] @@ -179,7 +186,14 @@ func CheckWebhookNameUniqueness(rv1 *bundle.RegistryV1) []error { webhookNameSetByType[wh.Type].Insert(wh.GenerateName) } - var errs []error + totalDuplicates := 0 + for _, duplicates := range duplicateWebhooksByType { + totalDuplicates += duplicates.Len() + } + if totalDuplicates == 0 { + return nil + } + errs := make([]error, 0, totalDuplicates) for _, whType := range slices.Sorted(maps.Keys(duplicateWebhooksByType)) { for _, webhookName := range slices.Sorted(slices.Values(duplicateWebhooksByType[whType].UnsortedList())) { errs = append(errs, fmt.Errorf("duplicate webhook '%s' of type '%s'", webhookName, whType)) @@ -267,7 +281,14 @@ func CheckWebhookNameIsDNS1123SubDomain(rv1 *bundle.RegistryV1) []error { } } - var errs []error + totalInvalid := 0 + for _, webhooks := range invalidWebhooksByType { + totalInvalid += len(webhooks) + } + if totalInvalid == 0 { + return nil + } + errs := make([]error, 0, totalInvalid) for _, whType := range slices.Sorted(maps.Keys(invalidWebhooksByType)) { for _, webhookName := range slices.Sorted(maps.Keys(invalidWebhooksByType[whType])) { errs = append(errs, fmt.Errorf("webhook of type '%s' has invalid name '%s': %s", whType, webhookName, strings.Join(invalidWebhooksByType[whType][webhookName], ","))) diff --git a/internal/operator-controller/rukpak/render/render.go b/internal/operator-controller/rukpak/render/render.go index f7e419c783..86eb2ff492 100644 --- a/internal/operator-controller/rukpak/render/render.go +++ b/internal/operator-controller/rukpak/render/render.go @@ -10,6 +10,7 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-controller/internal/operator-controller/config" "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" @@ -20,7 +21,7 @@ import ( type BundleValidator []func(v1 *bundle.RegistryV1) []error func (v BundleValidator) Validate(rv1 *bundle.RegistryV1) error { - var errs []error + errs := make([]error, 0, len(v)) for _, validator := range v { errs = append(errs, validator(rv1)...) } @@ -62,6 +63,9 @@ type Options struct { TargetNamespaces []string UniqueNameGenerator UniqueNameGenerator CertificateProvider CertificateProvider + // DeploymentConfig contains optional customizations to apply to CSV deployments. + // If nil, no customizations are applied. + DeploymentConfig *config.DeploymentConfig } func (o *Options) apply(opts ...Option) *Options { @@ -109,6 +113,14 @@ func WithCertificateProvider(provider CertificateProvider) Option { } } +// WithDeploymentConfig sets the deployment configuration to apply to CSV deployments. +// If deploymentConfig is nil, no customizations are applied. +func WithDeploymentConfig(deploymentConfig *config.DeploymentConfig) Option { + return func(o *Options) { + o.DeploymentConfig = deploymentConfig + } +} + type BundleRenderer struct { BundleValidator BundleValidator ResourceGenerators []ResourceGenerator diff --git a/internal/operator-controller/rukpak/render/render_test.go b/internal/operator-controller/rukpak/render/render_test.go index ca14598896..452f9f3fdb 100644 --- a/internal/operator-controller/rukpak/render/render_test.go +++ b/internal/operator-controller/rukpak/render/render_test.go @@ -13,6 +13,7 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-controller/internal/operator-controller/config" "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" @@ -382,3 +383,79 @@ func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) { require.NoError(t, val.Validate(nil)) require.Equal(t, "hi", actual) } + +func Test_WithDeploymentConfig(t *testing.T) { + t.Run("sets deployment config when provided", func(t *testing.T) { + expectedConfig := &config.DeploymentConfig{ + Env: []corev1.EnvVar{ + {Name: "TEST_ENV", Value: "test-value"}, + }, + } + + var receivedConfig *config.DeploymentConfig + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + receivedConfig = opts.DeploymentConfig + return nil, nil + }, + }, + } + + _, err := renderer.Render( + bundle.RegistryV1{ + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), + }, + "test-namespace", + render.WithDeploymentConfig(expectedConfig), + ) + + require.NoError(t, err) + require.Equal(t, expectedConfig, receivedConfig) + }) + + t.Run("deployment config is nil when not provided", func(t *testing.T) { + var receivedConfig *config.DeploymentConfig + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + receivedConfig = opts.DeploymentConfig + return nil, nil + }, + }, + } + + _, err := renderer.Render( + bundle.RegistryV1{ + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), + }, + "test-namespace", + ) + + require.NoError(t, err) + require.Nil(t, receivedConfig) + }) + + t.Run("deployment config is nil when explicitly set to nil", func(t *testing.T) { + var receivedConfig *config.DeploymentConfig + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + receivedConfig = opts.DeploymentConfig + return nil, nil + }, + }, + } + + _, err := renderer.Render( + bundle.RegistryV1{ + CSV: clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build(), + }, + "test-namespace", + render.WithDeploymentConfig(nil), + ) + + require.NoError(t, err) + require.Nil(t, receivedConfig) + }) +} diff --git a/internal/shared/util/error/terminal.go b/internal/shared/util/error/terminal.go index cd70d535f7..5c91012079 100644 --- a/internal/shared/util/error/terminal.go +++ b/internal/shared/util/error/terminal.go @@ -1,6 +1,61 @@ package error -import "sigs.k8s.io/controller-runtime/pkg/reconcile" +import ( + "errors" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// terminalErrorWithReason is an internal error type that carries a Reason field +// to provide more granular categorization of terminal errors for status conditions. +type terminalErrorWithReason struct { + reason string + err error +} + +func (e *terminalErrorWithReason) Error() string { + return e.err.Error() +} + +func (e *terminalErrorWithReason) Unwrap() error { + return e.err +} + +// NewTerminalError creates a terminal error with a specific reason. +// The error is wrapped with reconcile.TerminalError so controller-runtime +// recognizes it as terminal, while preserving the reason for status reporting. +// +// Example usage: +// +// return error.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("missing required field")) +// +// The reason can be extracted later using ExtractTerminalReason() when setting +// status conditions to provide more specific feedback than just "Blocked". +func NewTerminalError(reason string, err error) error { + return reconcile.TerminalError(&terminalErrorWithReason{ + reason: reason, + err: err, + }) +} + +// ExtractTerminalReason extracts the reason from a terminal error created with +// NewTerminalError. Returns the reason and true if found, or empty string and +// false if the error was not created with NewTerminalError. +// +// This allows setStatusProgressing to use specific reasons like "InvalidConfiguration" +// instead of the generic "Blocked" for all terminal errors. +func ExtractTerminalReason(err error) (string, bool) { + if err == nil { + return "", false + } + // Unwrap the reconcile.TerminalError wrapper first + unwrapped := errors.Unwrap(err) + var terr *terminalErrorWithReason + if errors.As(unwrapped, &terr) { + return terr.reason, true + } + return "", false +} func WrapTerminal(err error, isTerminal bool) error { if !isTerminal || err == nil { @@ -8,3 +63,22 @@ func WrapTerminal(err error, isTerminal bool) error { } return reconcile.TerminalError(err) } + +// UnwrapTerminal unwraps a TerminalError to get the underlying error without +// the "terminal error:" prefix that reconcile.TerminalError adds to the message. +// This is useful when displaying error messages in status conditions where the +// terminal/blocked nature is already conveyed by the condition Reason field. +// +// If err is not a TerminalError, it returns err unchanged. +// If err is nil, it returns nil. +func UnwrapTerminal(err error) error { + if err == nil { + return nil + } + if errors.Is(err, reconcile.TerminalError(nil)) { + if unwrapped := errors.Unwrap(err); unwrapped != nil { + return unwrapped + } + } + return err +} diff --git a/kind-config/kind-config-2node.yaml b/kind-config/kind-config-2node.yaml new file mode 100644 index 0000000000..5532a9932c --- /dev/null +++ b/kind-config/kind-config-2node.yaml @@ -0,0 +1,45 @@ +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +nodes: + - role: control-plane + extraPortMappings: + # e2e image registry service's NodePort + - containerPort: 30000 + 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 + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + taints: [] + extraMounts: + - hostPath: ./hack/kind-config/containerd/certs.d + containerPath: /etc/containerd/certs.d + - role: control-plane + kubeadmConfigPatches: + - | + kind: JoinConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + taints: [] + 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/kind-config.yaml b/kind-config/kind-config.yaml similarity index 100% rename from kind-config.yaml rename to kind-config/kind-config.yaml diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 862edac124..6536baf930 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -152,6 +152,10 @@ data: [[registry]] prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" + + [[registry]] + prefix = "dynamic-registry.operator-controller-e2e.svc.cluster.local:5000" + location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" kind: ConfigMap metadata: annotations: @@ -185,7 +189,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clustercatalogs.olm.operatorframework.io spec: @@ -211,7 +215,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -233,29 +237,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -263,35 +262,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -302,25 +299,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -403,12 +398,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -426,31 +420,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -511,11 +504,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -524,14 +515,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -565,12 +556,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -589,19 +579,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: @@ -628,7 +615,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensionrevisions.olm.operatorframework.io spec: @@ -791,6 +778,16 @@ spec: x-kubernetes-validations: - message: phases is immutable rule: self == oldSelf || oldSelf.size() == 0 + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer revision: description: |- revision is a required, immutable sequence number representing a specific revision @@ -903,7 +900,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensions.olm.operatorframework.io spec: @@ -959,31 +956,29 @@ spec: 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 optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is 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. + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. properties: configType: description: |- - configType is a required reference to the type of configuration source. + configType is required and specifies the type of configuration source. - Allowed values are "Inline" + The only allowed value is "Inline". - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. + When 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 contains JSON or YAML values specified directly in the ClusterExtension. - inline is used to specify arbitrary configuration values for the ClusterExtension. + It is used to specify arbitrary configuration values for the ClusterExtension. It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. The configuration values are validated at runtime against a JSON schema provided by the bundle. minProperties: 1 @@ -999,37 +994,35 @@ spec: : !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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -1051,16 +1044,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -1070,26 +1062,34 @@ spec: rule: self == oldSelf - message: namespace must be a valid DNS1123 label rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer serviceAccount: description: |- - serviceAccount is a reference to a ServiceAccount used to perform all interactions - with the cluster that are required to manage the extension. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -1118,11 +1118,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1133,30 +1133,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -1186,13 +1185,12 @@ spec: 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 specifies 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. + It 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 @@ -1219,12 +1217,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -1272,35 +1267,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -1378,13 +1372,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -1490,11 +1483,13 @@ spec: x-kubernetes-list-type: map conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. @@ -1503,12 +1498,12 @@ spec: When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out. - 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -1573,17 +1568,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -1593,8 +1587,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver @@ -2159,6 +2153,7 @@ spec: image: busybox:1.36 name: tar securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: @@ -2370,7 +2365,6 @@ spec: - --metrics-bind-address=:8443 - --pprof-bind-address=:6060 - --leader-elect - - --feature-gates=SingleOwnNamespaceInstallSupport=true - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 3ea459499f..0b750139d2 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -150,7 +150,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clustercatalogs.olm.operatorframework.io spec: @@ -176,7 +176,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -198,29 +198,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -228,35 +223,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -267,25 +260,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -368,12 +359,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -391,31 +381,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -476,11 +465,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -489,14 +476,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -530,12 +517,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -554,19 +540,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: @@ -593,7 +576,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensionrevisions.olm.operatorframework.io spec: @@ -756,6 +739,16 @@ spec: x-kubernetes-validations: - message: phases is immutable rule: self == oldSelf || oldSelf.size() == 0 + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer revision: description: |- revision is a required, immutable sequence number representing a specific revision @@ -868,7 +861,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: experimental name: clusterextensions.olm.operatorframework.io spec: @@ -924,31 +917,29 @@ spec: 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 optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is 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. + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. properties: configType: description: |- - configType is a required reference to the type of configuration source. + configType is required and specifies the type of configuration source. - Allowed values are "Inline" + The only allowed value is "Inline". - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. + When 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 contains JSON or YAML values specified directly in the ClusterExtension. - inline is used to specify arbitrary configuration values for the ClusterExtension. + It is used to specify arbitrary configuration values for the ClusterExtension. It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. The configuration values are validated at runtime against a JSON schema provided by the bundle. minProperties: 1 @@ -964,37 +955,35 @@ spec: : !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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -1016,16 +1005,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -1035,26 +1023,34 @@ spec: rule: self == oldSelf - message: namespace must be a valid DNS1123 label rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + progressDeadlineMinutes: + description: |- + progressDeadlineMinutes is an optional field that defines the maximum period + of time in minutes after which an installation should be considered failed and + require manual intervention. This functionality is disabled when no value + is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours). + format: int32 + maximum: 720 + minimum: 10 + type: integer serviceAccount: description: |- - serviceAccount is a reference to a ServiceAccount used to perform all interactions - with the cluster that are required to manage the extension. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -1083,11 +1079,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1098,30 +1094,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -1151,13 +1146,12 @@ spec: 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 specifies 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. + It 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 @@ -1184,12 +1178,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -1237,35 +1228,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -1343,13 +1333,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -1455,11 +1444,13 @@ spec: x-kubernetes-list-type: map conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. @@ -1468,12 +1459,12 @@ spec: When Progressing is True and Reason is RollingOut, the ClusterExtension has one or more ClusterExtensionRevisions in active roll out. - 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -1538,17 +1529,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -1558,8 +1548,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver @@ -2281,7 +2271,6 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=:8443 - --leader-elect - - --feature-gates=SingleOwnNamespaceInstallSupport=true - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml index 1aed38ba96..3ae2938d8b 100644 --- a/manifests/standard-e2e.yaml +++ b/manifests/standard-e2e.yaml @@ -152,6 +152,10 @@ data: [[registry]] prefix = "mirrored-registry.operator-controller-e2e.svc.cluster.local:5000" location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" + + [[registry]] + prefix = "dynamic-registry.operator-controller-e2e.svc.cluster.local:5000" + location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000" kind: ConfigMap metadata: annotations: @@ -185,7 +189,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clustercatalogs.olm.operatorframework.io spec: @@ -211,7 +215,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -233,29 +237,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -263,35 +262,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -302,25 +299,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -403,12 +398,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -426,31 +420,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -511,11 +504,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -524,14 +515,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -565,12 +556,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -589,19 +579,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: @@ -628,7 +615,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clusterextensions.olm.operatorframework.io spec: @@ -682,39 +669,75 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: + config: + description: |- + config is optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. + properties: + configType: + description: |- + configType is required and specifies the type of configuration source. + + The only allowed value is "Inline". + + When 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. + + It is used to specify arbitrary configuration values for the ClusterExtension. + It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. + The configuration values are validated at runtime against a JSON schema provided by the bundle. + minProperties: 1 + 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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -736,16 +759,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -757,24 +779,22 @@ spec: 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. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -803,11 +823,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -818,30 +838,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -871,13 +890,12 @@ spec: 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 specifies 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. + It 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 @@ -904,12 +922,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -957,35 +972,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -1063,13 +1077,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -1093,23 +1106,25 @@ spec: properties: conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -1174,17 +1189,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -1194,8 +1208,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver @@ -1760,6 +1774,7 @@ spec: image: busybox:1.36 name: tar securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: diff --git a/manifests/standard.yaml b/manifests/standard.yaml index 34cc579181..2fc75569ca 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -150,7 +150,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clustercatalogs.olm.operatorframework.io spec: @@ -176,7 +176,7 @@ spec: schema: openAPIV3Schema: description: |- - ClusterCatalog enables users to make File-Based Catalog (FBC) catalog data available to the cluster. + ClusterCatalog makes File-Based Catalog (FBC) data available to your cluster. For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs properties: apiVersion: @@ -198,29 +198,24 @@ spec: 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. + spec is a required field that defines the desired state of the ClusterCatalog. + The controller ensures that the 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. + availabilityMode is an optional field that defines how the ClusterCatalog is made available to clients on the cluster. - Allowed values are "Available" and "Unavailable" and omitted. + Allowed values are "Available", "Unavailable", or 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 "Available", the catalog contents are unpacked and served over the catalog content HTTP server. + Clients 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. + When set to "Unavailable", the catalog contents are no longer served over the catalog content HTTP server. + Treat this the same as if the ClusterCatalog does not exist. + Use "Unavailable" when you want to keep the ClusterCatalog but treat it as if it doesn't exist. enum: - Unavailable - Available @@ -228,35 +223,33 @@ spec: priority: default: 0 description: |- - priority allows the user to define a priority for a ClusterCatalog. - priority is optional. + priority is an optional field that defines a priority for this ClusterCatalog. - 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. + Clients use the ClusterCatalog priority as a tie-breaker between ClusterCatalogs that meet their requirements. + Higher numbers mean 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. + Clients decide how to handle scenarios where multiple ClusterCatalogs with the same priority meet their requirements. + Clients should prompt users for additional input to break the tie. - When omitted, the default priority is 0 because that is the zero value of integers. + When omitted, the default priority is 0. - 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. + Use negative numbers to specify a priority lower than the default. + Use positive numbers to specify a priority higher than the default. The lowest possible value is -2147483648. The highest possible value is 2147483647. format: int32 + maximum: 2147483647 + minimum: -2147483648 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. + source is a required field that defines the source of a catalog. + A catalog contains information on content that can be installed on a cluster. + The catalog source makes catalog contents discoverable and usable by other on-cluster components. + These components can present the content in a GUI dashboard or install 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: @@ -267,25 +260,23 @@ spec: 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. + image configures how catalog contents are sourced from an OCI image. + It 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. + pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content. + You cannot specify pollIntervalMinutes when ref is a digest-based reference. - When omitted, the image will not be polled for new content. + When omitted, the image is not 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. + ref is a required field that defines the reference to a container image containing catalog contents. + It cannot be more than 1000 characters. - A reference can be broken down into 3 parts - the domain, name, and identifier. + A reference has 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. @@ -368,12 +359,11 @@ spec: : true' type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", the ClusterCatalog content will be sourced from an OCI image. + When set to "Image", the ClusterCatalog content is 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 @@ -391,31 +381,30 @@ spec: 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 + status contains the following information about the state of the ClusterCatalog: + - Whether the catalog contents are being served via the catalog content HTTP server + - Whether 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. + conditions represents the current state of 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 Serving condition represents whether the catalog contents are being served via the HTTP(S) web server: + - When status is True and reason is Available, the catalog contents are being served. + - When status is False and reason is Unavailable, the catalog contents are not being served because the contents are not yet available. + - When status is False and reason is UserSpecifiedUnavailable, the catalog contents 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. + The Progressing condition represents whether the ClusterCatalog is progressing or is ready to progress towards a new state: + - When status is True and reason is Retrying, an error occurred that may be resolved on subsequent reconciliation attempts. + - When status is True and reason is Succeeded, the ClusterCatalog has successfully progressed to a new state and is ready to continue progressing. + - When status is False and reason is Blocked, an error occurred 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. + If the system initially fetched contents and polling identifies updates, both conditions can be active simultaneously: + - The Serving condition remains True with reason Available because the previous contents are still served via the HTTP(S) web server. + - The Progressing condition is True with reason Retrying because the system is working to serve the new version. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -476,11 +465,9 @@ spec: 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". + lastUnpacked represents the last time the catalog contents were extracted from their source format. + For example, when using an Image source, the OCI image is pulled and image layers are written to a file-system backed cache. + This extraction from the source format is called "unpacking". format: date-time type: string resolvedSource: @@ -489,14 +476,14 @@ spec: 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. + image contains resolution information for a catalog sourced from an image. + It 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. + The digest format allows you to use other tooling to fetch the exact OCI manifests + that were used to extract the catalog contents. maxLength: 1000 type: string x-kubernetes-validations: @@ -530,12 +517,11 @@ spec: type: object type: description: |- - type is a reference to the type of source the catalog is sourced from. - type is required. + type is a required field that specifies the type of source for the catalog. The only allowed value is "Image". - When set to "Image", information about the resolved image source will be set in the 'image' field. + When set to "Image", information about the resolved image source is set in the image field. enum: - Image type: string @@ -554,19 +540,16 @@ spec: properties: base: description: |- - base is a cluster-internal URL that provides endpoints for - accessing the content of the catalog. + base is a cluster-internal URL that provides endpoints for accessing the catalog content. - It is expected that clients append the path for the endpoint they wish - to access. + Clients should append the path for the endpoint they want to access. - Currently, only a single endpoint is served and is accessible at the path - /api/v1. + 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 + - /all - this endpoint returns the entire catalog contents in the FBC format - As the needs of users and clients of the evolve, new endpoints may be added. + New endpoints may be added as needs evolve. maxLength: 525 type: string x-kubernetes-validations: @@ -593,7 +576,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.19.0 + controller-gen.kubebuilder.io/version: v0.20.0 olm.operatorframework.io/generator: standard name: clusterextensions.olm.operatorframework.io spec: @@ -647,39 +630,75 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: + config: + description: |- + config is optional and specifies bundle-specific configuration. + Configuration is bundle-specific and a bundle may provide a configuration schema. + When not specified, the default configuration of the resolved bundle is used. + + config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide + a configuration schema the bundle is deemed to not be configurable. More information on how + to configure bundles can be found in the OLM documentation associated with your current OLM version. + properties: + configType: + description: |- + configType is required and specifies the type of configuration source. + + The only allowed value is "Inline". + + When 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. + + It is used to specify arbitrary configuration values for the ClusterExtension. + It must be set if configType is 'Inline' and must be a valid JSON/YAML object containing at least one property. + The configuration values are validated at runtime against a JSON schema provided by the bundle. + minProperties: 1 + 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. + install is optional and configures 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. + preflight is optional and configures the checks that 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. + When not specified, the default configuration is used. properties: crdUpgradeSafety: description: |- - crdUpgradeSafety is used to configure the CRD Upgrade Safety pre-flight - checks that run prior to upgrades of installed content. + crdUpgradeSafety configures the CRD Upgrade Safety pre-flight checks that run + before upgrades of installed content. - The CRD Upgrade Safety pre-flight check safeguards from unintended - consequences of upgrading a CRD, such as data loss. + 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. + enforcement is required and configures 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 "None", the CRD Upgrade Safety pre-flight check is skipped during an upgrade operation. + Use this option 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. + When set to "Strict", the CRD Upgrade Safety pre-flight check runs during an upgrade operation. enum: - None - Strict @@ -701,16 +720,15 @@ spec: 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. + namespace specifies a Kubernetes namespace. + This is the namespace where 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 + The namespace field 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 @@ -722,24 +740,22 @@ spec: 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. + serviceAccount specifies 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. + The serviceAccount field 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. + name is a required, immutable reference to the name of the ServiceAccount 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. + The name field 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 @@ -768,11 +784,11 @@ spec: 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. + source is required and selects the installation source of content for this ClusterExtension. + Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType, and setting the - sourcetype to "Catalog" requires the catalog field to also be defined. + Catalog is currently the only implemented sourceType. + Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -783,30 +799,29 @@ spec: 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. + catalog configures how information is sourced from a catalog. + It 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. + channels is optional and specifies 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. + 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. + 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. + You can specify no more than 256 channels. - 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: + When specified, it constrains 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 + - Only bundles that exist in channel "foo" AND satisfy the version range comparison are considered installable + - Automatic upgrades are 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. + When unspecified, upgrade edges across all channels are used to identify valid automatic upgrade paths. Some examples of valid values are: - 1.1.x @@ -836,13 +851,12 @@ spec: 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 specifies 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. + It 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 @@ -869,12 +883,9 @@ spec: 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. + selector is optional and filters the set of ClusterCatalogs used in the bundle selection process. - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. + When unspecified, all ClusterCatalogs are used in the bundle selection process. properties: matchExpressions: description: matchExpressions is a list of label selector @@ -922,35 +933,34 @@ spec: 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. + upgradeConstraintPolicy is optional and controls whether the upgrade paths defined in the catalog + are enforced for the package referenced in the packageName field. - Allowed values are: "CatalogProvided" or "SelfCertified", or omitted. + Allowed values are "CatalogProvided", "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 set to "CatalogProvided", automatic upgrades 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 set to "SelfCertified", the upgrade constraints specified by the package author are ignored. + This allows 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. + Use this option only if you have independently verified the changes. - When this field is omitted, the default value is "CatalogProvided". + When 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. + version is an optional semver constraint (a specific version or range of versions). + When unspecified, the latest version available is 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 (||). + Version ranges are composed of comma- or space-delimited values and one or more comparison operators, + known as comparison strings. + You can add additional comparison strings using the OR operator (||). # Range Comparisons @@ -1028,13 +1038,12 @@ spec: type: object sourceType: description: |- - sourceType is a required reference to the type of install source. + sourceType is required and specifies the type of install source. - Allowed values are "Catalog" + The only allowed value is "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 set to "Catalog", information for determining the appropriate bundle of content to install + is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog @@ -1058,23 +1067,25 @@ spec: properties: conditions: description: |- + conditions represents the current state of the ClusterExtension. + 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 Installed condition represents whether 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. + When the ClusterExtension is sourced from a catalog, it surfaces deprecation conditions based on catalog metadata. + These are indications from a package owner to guide users away from a particular package, channel, or bundle: + - BundleDeprecated is True if the installed bundle is marked deprecated, False if not deprecated, or Unknown if no bundle is installed yet or if catalog data is unavailable. + - ChannelDeprecated is True if any requested channel is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - PackageDeprecated is True if the requested package is marked deprecated, False if not deprecated, or Unknown if catalog data is unavailable. + - Deprecated is a rollup condition that is True when any deprecation exists, False when none exist, or Unknown when catalog data is unavailable. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -1139,17 +1150,16 @@ spec: properties: bundle: description: |- - bundle is a required field which represents the identifying attributes of a bundle. + bundle is required and 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. + 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. + 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. @@ -1159,8 +1169,8 @@ spec: 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/. + version is required and references the version that this bundle represents. + It follows the semantic versioning standard as defined in https://semver.org/. type: string x-kubernetes-validations: - message: version must be well-formed semver diff --git a/requirements.txt b/requirements.txt index 35b0e3fc6b..9c7df703a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Babel==2.17.0 -beautifulsoup4==4.14.2 -certifi==2025.11.12 +beautifulsoup4==4.14.3 +certifi==2026.1.4 charset-normalizer==3.4.4 click==8.3.1 colorama==0.4.6 @@ -9,27 +9,27 @@ ghp-import==2.1.0 idna==3.11 Jinja2==3.1.6 lxml==6.0.2 -Markdown==3.10 +Markdown==3.10.1 markdown2==2.5.4 MarkupSafe==3.0.3 mergedeep==1.3.4 mkdocs==1.6.1 -mkdocs-material==9.7.0 +mkdocs-material==9.7.1 mkdocs-material-extensions==1.3.1 -packaging==25.0 +packaging==26.0 paginate==0.5.7 -pathspec==0.12.1 -platformdirs==4.5.0 +pathspec==1.0.4 +platformdirs==4.5.1 Pygments==2.19.2 -pymdown-extensions==10.17.2 +pymdown-extensions==10.20.1 pyquery==2.0.1 python-dateutil==2.9.0.post0 PyYAML==6.0.3 pyyaml_env_tag==1.1 readtime==3.0.0 -regex==2025.11.3 +regex==2026.1.15 requests==2.32.5 six==1.17.0 -soupsieve==2.8 -urllib3==2.6.0 +soupsieve==2.8.3 +urllib3==2.6.3 watchdog==6.0.0 diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000000..c3483e5182 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,350 @@ +# E2E Tests - Godog Framework + +This directory contains end-to-end (e2e) tests, written using the [Godog](https://github.com/cucumber/godog) framework. + +## Overview + +### What is Godog/BDD/Cucumber? + +Godog is a Behavior-Driven Development (BDD) framework that allows you to write tests in a human-readable format called +[Gherkin](https://cucumber.io/docs/gherkin/reference/). Tests are written as scenarios using Given-When-Then syntax, making them accessible to both technical and +non-technical stakeholders. + +**Benefits:** + +- **Readable**: Tests serve as living documentation +- **Maintainable**: Reusable step definitions reduce code duplication +- **Collaborative**: Product owners and developers share the same test specifications +- **Structured**: Clear separation between test scenarios and implementation + +## Project Structure + +``` +test/e2e/ +├── README.md # This file +├── features_test.go # Test runner and suite initialization +├── features/ # Gherkin feature files +│ ├── install.feature # ClusterExtension installation scenarios +│ ├── update.feature # ClusterExtension update scenarios +│ ├── recover.feature # Recovery scenarios +│ ├── status.feature # ClusterExtension status scenarios +│ └── metrics.feature # Metrics endpoint scenarios +└── steps/ # Step definitions and test utilities + ├── steps.go # Step definition implementations + ├── hooks.go # Test hooks and scenario context + └── testdata/ # Test data (RBAC templates, catalogs) + ├── rbac-template.yaml + ├── cluster-admin-rbac-template.yaml + ├── metrics-reader-rbac-template.yaml + ├── test-catalog-template.yaml + ├── extra-catalog-template.yaml + └── ... +``` + +## Architecture + +### 1. Test Runner (`features_test.go`) + +The main test entry point that configures and runs the Godog test suite. + +### 2. Feature Files (`features/*.feature`) + +Gherkin files that describe test scenarios in natural language. + +**Structure:** + +```gherkin +Feature: [Feature Name] + [Feature description] + + Background: + [Common setup steps for all scenarios] + + Scenario: [Scenario Name] + Given [precondition] + When [action] + Then [expected result] + And [additional assertions] +``` + +**Example:** + +```gherkin +Feature: Install ClusterExtension + + Background: + Given OLM is available + And "test" catalog serves bundles + And Service account "olm-sa" with needed permissions is available in test namespace + + Scenario: Install latest available version from the default channel + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + ... + """ + Then ClusterExtension is rolled out + And ClusterExtension is available +``` + +### 3. Step Definitions (`steps/steps.go`) + +Go functions that implement the steps defined in feature files. Each step is registered with a regex pattern that +matches the Gherkin text. + +**Registration:** + +```go +func RegisterSteps(sc *godog.ScenarioContext) { +sc.Step(`^OLM is available$`, OLMisAvailable) +sc.Step(`^bundle "([^"]+)" is installed in version "([^"]+)"$`, BundleInstalled) +sc.Step(`^ClusterExtension is applied$`, ResourceIsApplied) +// ... more steps +} +``` + +**Step Implementation Pattern:** + +```go +func BundleInstalled(ctx context.Context, name, version string) error { + sc := scenarioCtx(ctx) + waitFor(ctx, func() bool { + v, err := kubectl("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.install.bundle}") + if err != nil { + return false + } + var bundle map[string]interface{} + json.Unmarshal([]byte(v), &bundle) + return bundle["name"] == name && bundle["version"] == version + }) + return nil +} +``` + +### 4. Hooks and Context (`steps/hooks.go`) + +Manages test lifecycle and scenario-specific context. + +**Hooks:** + +- `CheckFeatureTags`: Skips scenarios based on feature gate tags (e.g., `@WebhookProviderCertManager`) +- `CreateScenarioContext`: Creates unique namespace and names for each scenario +- `ScenarioCleanup`: Cleans up resources after each scenario + +**Variable Substitution:** + +Replaces `${TEST_NAMESPACE}`, `${NAME}`, and `${CATALOG_IMG}` with scenario-specific values. + +## Writing Tests + +### 1. Create a Feature File + +Create a new `.feature` file in `test/e2e/features/`: + +```gherkin +Feature: Your Feature Name + Description of what this feature tests + + Background: + Given OLM is available + And "test" catalog serves bundles + + Scenario: Your scenario description + When [some action] + Then [expected outcome] +``` + +### 2. Implement Step Definitions + +Add step implementations in `steps/steps.go`: + +```go +func RegisterSteps(sc *godog.ScenarioContext) { + // ... existing steps + sc.Step(`^your step pattern "([^"]+)"$`, YourStepFunction) +} + +func YourStepFunction(ctx context.Context, param string) error { + sc := scenarioCtx(ctx) + // Implementation + return nil +} +``` + +### 3. Use Existing Steps + +Leverage existing steps for common operations: + +- **Setup**: `Given OLM is available`, `And "test" catalog serves bundles` +- **Resource Management**: `When ClusterExtension is applied`, `And resource is applied` +- **Assertions**: `Then ClusterExtension is available`, `And bundle "..." is installed` +- **Conditions**: `Then ClusterExtension reports Progressing as True with Reason Retrying:` + +### 4. Variable Substitution + +Use these variables in YAML templates: + +- `${NAME}`: Scenario-specific ClusterExtension name (e.g., `ce-123`) +- `${TEST_NAMESPACE}`: Scenario-specific namespace (e.g., `ns-123`) +- `${CATALOG_IMG}`: Catalog image reference (defaults to in-cluster registry, overridable via `CATALOG_IMG` env var) + +### 5. Feature Tags + +Use tags to conditionally run scenarios based on feature gates: + +```gherkin +@WebhookProviderCertManager +Scenario: Install operator having webhooks +``` + +Scenarios are skipped if the feature gate is not enabled on the deployed controller. + +## Running Tests + +### Run All Tests + +```bash +make test-e2e +``` + +or + +```bash +make test-experimental-e2e +``` + + +### Run Specific Feature + +```bash +go test test/e2e/features_test.go -- features/install.feature +``` + +### Run Specific Scenario by Tag + +```bash +go test test/e2e/features_test.go --godog.tags="@WebhookProviderCertManager" +``` + +### Run with Debug Logging + +```bash +go test -v test/e2e/features_test.go --log.debug +``` + +### CLI Options + +Godog options can be passed after `--`: + +```bash +go test test/e2e/features_test.go \ + --godog.format=pretty \ + --godog.tags="@WebhookProviderCertManager" +``` + +Available formats: `pretty`, `cucumber`, `progress`, `junit` + +**Custom Flags:** + +- `--log.debug`: Enable debug logging (development mode) +- `--k8s.cli=`: Specify path to Kubernetes CLI (default: `kubectl`) + - Useful for using `oc` or a specific kubectl binary + +**Example:** + +```bash +go test test/e2e/features_test.go --log.debug --k8s.cli=oc +``` + +### Environment Variables + +- `KUBECONFIG`: Path to kubeconfig file (defaults to `~/.kube/config`) +- `E2E_SUMMARY_OUTPUT`: Path to write test summary (optional) +- `CATALOG_IMG`: Override default catalog image reference (optional) +- `LOCAL_REGISTRY_HOST`: Local registry host for catalog images + +## Design Patterns + +### 1. Scenario Isolation + +Each scenario runs in its own namespace with unique resource names, ensuring complete isolation: + +- Namespace: `ns-{scenario-id}` +- ClusterExtension: `ce-{scenario-id}` + +### 2. Automatic Cleanup + +The `ScenarioCleanup` hook ensures all resources are deleted after each scenario: + +- Kills background processes (e.g., kubectl port-forward) +- Deletes ClusterExtensions +- Deletes namespaces +- Deletes added resources + +### 3. Declarative Resource Management + +Resources are managed declaratively using YAML templates embedded in feature files as docstrings: + +```gherkin +When ClusterExtension is applied +""" + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + ... + """ +``` + +### 4. Polling with Timeouts + +All asynchronous operations use `waitFor` with consistent timeout (300s) and tick (1s): + +```go +waitFor(ctx, func() bool { + // Check condition + return conditionMet +}) +``` + +### 5. Feature Gate Detection + +Tests automatically detect enabled feature gates from the running controller and skip scenarios that require disabled +features. + +## Common Step Patterns + +A list of available, implemented steps can be obtained by running: + +```shell +go test test/e2e/features_test.go -d +``` + +## Best Practices + +1. **Keep scenarios focused**: Each scenario should test one specific behavior +2. **Use Background wisely**: Common setup steps belong in Background +3. **Reuse steps**: Leverage existing step definitions before creating new ones +4. **Meaningful names**: Scenario names should clearly describe what is being tested +5. **Avoid implementation details**: Focus on behavior, not implementation + +## References + +- [Godog Documentation](https://github.com/cucumber/godog) +- [Gherkin Reference](https://cucumber.io/docs/gherkin/reference/) +- [Cucumber Best Practices](https://cucumber.io/docs/guides/10-minute-tutorial/) diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go deleted file mode 100644 index b1994d7e03..0000000000 --- a/test/e2e/cluster_extension_install_test.go +++ /dev/null @@ -1,798 +0,0 @@ -package e2e - -import ( - "context" - "fmt" - "os" - "slices" - "testing" - "time" - - "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" - 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/types" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/utils/ptr" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" - . "github.com/operator-framework/operator-controller/test/helpers" -) - -const ( - artifactName = "operator-controller-e2e" - pollDuration = time.Minute - pollInterval = time.Second - testCatalogRefEnvVar = "CATALOG_IMG" - testCatalogName = "test-catalog" -) - -func TestClusterExtensionInstallRegistry(t *testing.T) { - type testCase struct { - name string - packageName string - } - for _, tc := range []testCase{ - { - name: "no registry configuration necessary", - packageName: "test", - }, - { - // NOTE: This test requires an extra configuration in /etc/containers/registries.conf, which is mounted - // 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", - packageName: "test-mirrored", - }, - } { - t.Run(tc.name, func(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 testutil.CollectTestArtifacts(t, artifactName, c, cfg) - - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: tc.packageName, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, - }, - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.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") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - 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) { - 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("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.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 }}") - }) - } -} - -func TestClusterExtensionInstallRegistryDynamic(t *testing.T) { - // NOTE: Like 'TestClusterExtensionInstallRegistry', this test also requires extra configuration in /etc/containers/registries.conf - packageName := "dynamic" - - 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 testutil.CollectTestArtifacts(t, artifactName, c, cfg) - - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: packageName, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, - }, - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, - }, - } - t.Log("It updates the registries.conf file contents") - cm := corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "e2e-registries-conf", - Namespace: "olmv1-system", - }, - Data: map[string]string{ - "registries.conf": `[[registry]] -prefix = "dynamic-registry.operator-controller-e2e.svc.cluster.local:5000" -location = "docker-registry.operator-controller-e2e.svc.cluster.local:5000"`, - }, - } - require.NoError(t, c.Update(context.Background(), &cm)) - - 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") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - 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 - // files to update from the configmap change. - // The theoretical max time is the kubelet sync period of 1 minute + - // ConfigMap cache TTL of 1 minute = 2 minutes - t.Log("By eventually reporting progressing as 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.TypeProgressing) - 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) { - 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.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) - extraCatalogName := fmt.Sprintf("extra-test-catalog-%s", rand.String(8)) - extraCatalog, err := CreateTestCatalog(context.Background(), extraCatalogName, os.Getenv(testCatalogRefEnvVar)) - require.NoError(t, err) - - defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) - defer testutil.CollectTestArtifacts(t, artifactName, c, cfg) - defer func(cat *ocv1.ClusterCatalog) { - 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) - }(extraCatalog) - - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: "test", - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, - }, - } - t.Log("It resolves to multiple bundle paths") - t.Log("By creating the ClusterExtension resource") - require.NoError(t, c.Create(context.Background(), clusterExtension)) - - t.Log("By eventually reporting a failed resolution with multiple bundles") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - 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) { - 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) - // Catalog names are sorted alphabetically in the error message - catalogs := []string{extensionCatalog.Name, extraCatalog.Name} - slices.Sort(catalogs) - expectedMessage := fmt.Sprintf("in multiple catalogs with the same priority %v", catalogs) - require.Contains(ct, cond.Message, expectedMessage) - }, pollDuration, pollInterval) -} - -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) - defer testutil.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", - // No Selector since this is an exact version match - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, - }, - } - require.NoError(t, c.Create(context.Background(), clusterExtension)) - t.Log("By eventually reporting a successful installation") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - 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", - }}, - clusterExtension.Status.Install, - ) - - 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 does not allow to upgrade the ClusterExtension to a non-successor version") - t.Log("By updating the 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" - require.NoError(t, c.Update(context.Background(), clusterExtension)) - t.Log("By eventually reporting an unsatisfiable resolution") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - 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) { - 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, 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) -} - -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) - defer testutil.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 updating the 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) -} - -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) - defer testutil.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 does allow to upgrade the ClusterExtension to any of the successor versions within non-zero major version") - t.Log("By updating the ClusterExtension resource by skipping versions") - // 1.0.1 replaces 1.0.0 in the test catalog - clusterExtension.Spec.Source.Catalog.Version = "1.0.1" - 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) { - 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 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) - defer testutil.CollectTestArtifacts(t, artifactName, c, cfg) - - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: "test", - Selector: &metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "olm.operatorframework.io/metadata.name", - Operator: metav1.LabelSelectorOpIn, - Values: []string{extensionCatalog.Name}, - }, - }, - }, - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.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 reporting a successful resolution and bundle path") - 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) - - // 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("CLUSTER_REGISTRY_HOST")) - err := patchTestCatalog(context.Background(), extensionCatalog.Name, updatedCatalogImage) - require.NoError(t, err) - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, 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 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.Contains(ct, clusterExtension.Status.Install.Bundle.Version, "1.3.0") - }, pollDuration, pollInterval) -} - -func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { - t.Log("When a cluster extension is installed from a catalog") - t.Log("It resolves again when a new catalog is available") - - // Tag the image with the new tag - var err error - 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 - catalogName := fmt.Sprintf("test-catalog-%s", rand.String(8)) - latestCatalogImage := fmt.Sprintf("%s/e2e/test-catalog:latest", os.Getenv("CLUSTER_REGISTRY_HOST")) - extensionCatalog, err := CreateTestCatalog(context.Background(), catalogName, latestCatalogImage) - require.NoError(t, err) - clusterExtensionName := fmt.Sprintf("clusterextension-%s", rand.String(8)) - clusterExtension := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterExtensionName, - }, - } - ns, err := CreateNamespace(context.Background(), clusterExtensionName) - require.NoError(t, err) - 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 testutil.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: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.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 reporting a successful resolution and bundle path") - 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) - - // 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("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) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.Name}, 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 eventually reporting a successful resolution and bundle path") - 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 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) - defer testutil.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: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, - }, - } - t.Log("It installs 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 reporting a successful installation") - 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") - }, pollDuration, pollInterval) - - t.Log("By deleting a managed resource") - testConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-configmap", - Namespace: clusterExtension.Spec.Namespace, - }, - } - require.NoError(t, c.Delete(context.Background(), testConfigMap)) - - t.Log("By eventually re-creating the managed resource") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: testConfigMap.Name, Namespace: testConfigMap.Namespace}, testConfigMap)) - }, pollDuration, pollInterval) -} - -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") - - t.Log("By not creating the Namespace and ServiceAccount") - clusterExtension, extensionCatalog := TestInitClusterExtensionClusterCatalog(t) - - defer TestCleanup(t, extensionCatalog, clusterExtension, nil, nil) - defer testutil.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("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) { - 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 testutil.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) { - 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 failing to install the package successfully due to no adoption support") - 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.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 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 deleting the Deployment. - 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) -} diff --git a/test/e2e/cluster_extension_revision_test.go b/test/e2e/cluster_extension_revision_test.go deleted file mode 100644 index 5c21e66ab0..0000000000 --- a/test/e2e/cluster_extension_revision_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package e2e - -import ( - "context" - "fmt" - "os" - "slices" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/tools/remotecommand" - 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/features" - . "github.com/operator-framework/operator-controller/internal/shared/util/test" - . "github.com/operator-framework/operator-controller/test/helpers" -) - -func TestClusterExtensionRevision(t *testing.T) { - SkipIfFeatureGateDisabled(t, string(features.BoxcutterRuntime)) - 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 CollectTestArtifacts(t, artifactName, c, cfg) - - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: "test", - Version: "1.0.1", - // we would also like to force upgrade to 1.0.2, which is not within the upgrade path - UpgradeConstraintPolicy: ocv1.UpgradeConstraintPolicySelfCertified, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, - }, - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.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") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - }, pollDuration, pollInterval) - - t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions") - var clusterExtensionRevision ocv1.ClusterExtensionRevision - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By eventually reporting progressing as 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.TypeProgressing) - 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) { - 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.Bundle) - require.Len(ct, clusterExtension.Status.ActiveRevisions, 1) - require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, clusterExtensionRevision.Name) - require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions) - }, pollDuration, pollInterval) - - t.Log("Check Deployment Availability Probe") - t.Log("By making the operator pod not ready") - podName := getPodName(t, clusterExtension.Spec.Namespace, client.MatchingLabels{"app": "olme2etest"}) - podExec(t, clusterExtension.Spec.Namespace, podName, []string{"rm", "/var/www/ready"}) - - t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:False:ProbeFailure conditions") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionFalse, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By propagating Available:False to ClusterExtension") - 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.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionFalse, cond.Status) - }, pollDuration, pollInterval) - - t.Log("By making the operator pod ready") - podName = getPodName(t, clusterExtension.Spec.Namespace, client.MatchingLabels{"app": "olme2etest"}) - podExec(t, clusterExtension.Spec.Namespace, podName, []string{"touch", "/var/www/ready"}) - - t.Log("By revision-1 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By propagating Available:True to ClusterExtension") - 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.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - }, pollDuration, pollInterval) - - t.Log("Check archiving") - t.Log("By upgrading the cluster extension to v1.2.0") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - clusterExtension.Spec.Source.Catalog.Version = "1.2.0" - require.NoError(t, c.Update(context.Background(), clusterExtension)) - }, pollDuration, pollInterval) - - t.Log("By revision-2 eventually reporting Progressing:True:Succeeded and Available:True:ProbesSucceeded conditions") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-2", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbesSucceeded, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By eventually reporting progressing, available, and installed as 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.TypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - 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.Bundle) - }, pollDuration, pollInterval) - - t.Log("By revision-1 eventually reporting Progressing:False:Archived and Available:Unknown:Archived conditions") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-1", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionFalse, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionUnknown, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonArchived, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By upgrading the cluster extension to v1.0.2 containing bad image reference") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - clusterExtension.Spec.Source.Catalog.Version = "1.0.2" - require.NoError(t, c.Update(context.Background(), clusterExtension)) - }, pollDuration, pollInterval) - - t.Log("By revision-3 eventually reporting Progressing:True:Succeeded and Available:False:ProbeFailure conditions") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: fmt.Sprintf("%s-3", clusterExtension.Name)}, &clusterExtensionRevision)) - cond := apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - - cond = apimeta.FindStatusCondition(clusterExtensionRevision.Status.Conditions, ocv1.ClusterExtensionRevisionTypeAvailable) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionFalse, cond.Status) - require.Equal(ct, ocv1.ClusterExtensionRevisionReasonProbeFailure, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("By eventually reporting more than one active revision") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - require.Len(ct, clusterExtension.Status.ActiveRevisions, 2) - require.Equal(ct, clusterExtension.Status.ActiveRevisions[0].Name, fmt.Sprintf("%s-2", clusterExtension.Name)) - require.Equal(ct, clusterExtension.Status.ActiveRevisions[1].Name, fmt.Sprintf("%s-3", clusterExtension.Name)) - require.Empty(ct, clusterExtension.Status.ActiveRevisions[0].Conditions) - require.NotEmpty(ct, clusterExtension.Status.ActiveRevisions[1].Conditions) - }, pollDuration, pollInterval) -} - -func getPodName(t *testing.T, podNamespace string, matchingLabels client.MatchingLabels) string { - var podList corev1.PodList - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.List(context.Background(), &podList, client.InNamespace(podNamespace), matchingLabels)) - podList.Items = slices.DeleteFunc(podList.Items, func(pod corev1.Pod) bool { - // Ignore terminating pods - return pod.DeletionTimestamp != nil - }) - require.Len(ct, podList.Items, 1) - }, pollDuration, pollInterval) - return podList.Items[0].Name -} - -func podExec(t *testing.T, podNamespace string, podName string, cmd []string) { - req := cs.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec") - req.VersionedParams(&corev1.PodExecOptions{ - Command: cmd, - Stdout: true, - }, scheme.ParameterCodec) - exec, err := remotecommand.NewSPDYExecutor(ctrl.GetConfigOrDie(), "POST", req.URL()) - require.NoError(t, err) - err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{Stdout: os.Stdout}) - require.NoError(t, err) -} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go deleted file mode 100644 index 847f5c7535..0000000000 --- a/test/e2e/e2e_suite_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package e2e - -import ( - "context" - "fmt" - "os" - "testing" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - 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" - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" -) - -var ( - cfg *rest.Config - c client.Client - cs *kubernetes.Clientset -) - -const ( - testSummaryOutputEnvVar = "E2E_SUMMARY_OUTPUT" - latestImageTag = "latest" -) - -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) - - cs, err = kubernetes.NewForConfig(cfg) - utilruntime.Must(err) - - res := m.Run() - path := os.Getenv(testSummaryOutputEnvVar) - if path == "" { - fmt.Printf("Note: E2E_SUMMARY_OUTPUT is unset; skipping summary generation") - } else { - err = testutil.PrintSummary(path) - if err != nil { - // Fail the run if alerts are found - fmt.Printf("%v", err) - os.Exit(1) - } - } - os.Exit(res) -} - -// patchTestCatalog will patch the existing clusterCatalog on the test cluster, provided -// the context, catalog name, and the image reference. It returns an error -// if any errors occurred while updating the catalog. -func patchTestCatalog(ctx context.Context, name string, newImageRef string) error { - // Fetch the existing ClusterCatalog - catalog := &ocv1.ClusterCatalog{} - err := c.Get(ctx, client.ObjectKey{Name: name}, catalog) - if err != nil { - return err - } - - // Update the ImageRef - catalog.Spec.Source.Image.Ref = newImageRef - - // Patch the ClusterCatalog - err = c.Update(ctx, catalog) - if err != nil { - return err - } - - return err -} diff --git a/test/e2e/features/install.feature b/test/e2e/features/install.feature new file mode 100644 index 0000000000..23f75c546a --- /dev/null +++ b/test/e2e/features/install.feature @@ -0,0 +1,445 @@ +Feature: Install ClusterExtension + + As an OLM user I would like to install a cluster extension from catalog + or get an appropriate information in case of an error. + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + + Scenario: Install latest available version + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + And resource "networkpolicy/test-operator-network-policy" is installed + And resource "configmap/test-configmap" is installed + And resource "deployment/test-operator" is installed + + @mirrored-registry + Scenario Outline: Install latest available version from mirrored registry + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And bundle "-operator.1.2.0" is installed in version "1.2.0" + And resource "networkpolicy/test-operator-network-policy" is installed + And resource "configmap/test-configmap" is installed + And resource "deployment/test-operator" is installed + + Examples: + | package-name | + | test-mirrored | + | dynamic | + + + Scenario: Report that bundle cannot be installed when it exists in multiple catalogs with same priority + Given ClusterCatalog "extra" serves bundles + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + """ + Then ClusterExtension reports Progressing as True with Reason Retrying and Message: + """ + found bundles for package "test" in multiple catalogs with the same priority [extra-catalog test-catalog] + """ + + @SingleOwnNamespaceInstallSupport + Scenario: watchNamespace config is required for extension supporting single namespace + Given ServiceAccount "olm-admin" in test namespace is cluster admin + And resource is applied + """ + apiVersion: v1 + kind: Namespace + metadata: + name: single-namespace-operator-target + """ + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + source: + sourceType: Catalog + catalog: + packageName: single-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as False with Reason InvalidConfiguration and Message: + """ + error for resolved bundle "single-namespace-operator.1.0.0" with version "1.0.0": + invalid ClusterExtension configuration: invalid configuration: required field "watchNamespace" is missing + """ + When ClusterExtension is updated to set config.watchNamespace field + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + config: + configType: Inline + inline: + watchNamespace: single-namespace-operator-target # added + source: + sourceType: Catalog + catalog: + packageName: single-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension reports Installed as True + And bundle "single-namespace-operator.1.0.0" is installed in version "1.0.0" + And operator "single-namespace-operator" target namespace is "single-namespace-operator-target" + + @SingleOwnNamespaceInstallSupport + Scenario: watchNamespace config is required for extension supporting own namespace + Given ServiceAccount "olm-admin" in test namespace is cluster admin + And ClusterExtension is applied without the watchNamespace configuration + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + source: + sourceType: Catalog + catalog: + packageName: own-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as False with Reason InvalidConfiguration and Message: + """ + error for resolved bundle "own-namespace-operator.1.0.0" with version + "1.0.0": invalid ClusterExtension configuration: invalid configuration: required + field "watchNamespace" is missing + """ + And ClusterExtension is updated to include the watchNamespace configuration + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + config: + configType: Inline + inline: + watchNamespace: some-ns # added, but not own namespace + source: + sourceType: Catalog + catalog: + packageName: own-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as False with Reason InvalidConfiguration and Message: + """ + error for resolved bundle "own-namespace-operator.1.0.0" with version + "1.0.0": invalid ClusterExtension configuration: invalid configuration: invalid + format for field "watchNamespace": 'some-ns' is not valid ownNamespaceInstallMode: + invalid value "some-ns": must be "${TEST_NAMESPACE}" (the namespace where the + operator is installed) because this operator only supports OwnNamespace install mode + """ + When ClusterExtension is updated to set watchNamespace to own namespace value + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + config: + configType: Inline + inline: + watchNamespace: ${TEST_NAMESPACE} # own namespace + source: + sourceType: Catalog + catalog: + packageName: own-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And operator "own-namespace-operator" target namespace is "${TEST_NAMESPACE}" + + @WebhookProviderCertManager + Scenario: Install operator having webhooks + Given ServiceAccount "olm-admin" in test namespace is cluster admin + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + source: + sourceType: Catalog + catalog: + packageName: webhook-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And resource apply fails with error msg containing "Invalid value: false: Spec.Valid must be true" + """ + apiVersion: webhook.operators.coreos.io/v1 + kind: WebhookTest + metadata: + name: ${NAME} + namespace: ${TEST_NAMESPACE} + spec: + valid: false # webhook rejects it as invalid value + """ + And resource is applied + """ + apiVersion: webhook.operators.coreos.io/v1 + kind: WebhookTest + metadata: + name: ${NAME} + namespace: ${TEST_NAMESPACE} + spec: + valid: true + """ + And resource "webhooktest/${NAME}" matches + """ + apiVersion: webhook.operators.coreos.io/v2 + kind: WebhookTest + metadata: + name: ${NAME} + namespace: ${TEST_NAMESPACE} + spec: + conversion: + valid: true + mutate: true + """ + And resource "webhooktest.v1.webhook.operators.coreos.io/${NAME}" matches + """ + apiVersion: webhook.operators.coreos.io/v1 + kind: WebhookTest + metadata: + name: ${NAME} + namespace: ${TEST_NAMESPACE} + spec: + valid: true + mutate: true + """ + + @SingleOwnNamespaceInstallSupport + Scenario: Report failure when watchNamespace has invalid DNS-1123 name + Given ServiceAccount "olm-admin" in test namespace is cluster admin + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + config: + configType: Inline + inline: + watchNamespace: invalid-namespace- + source: + sourceType: Catalog + catalog: + packageName: single-namespace-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension reports Progressing as False with Reason InvalidConfiguration and Message includes: + """ + invalid ClusterExtension configuration: invalid configuration: field "watchNamespace" must match pattern + """ + + @SingleOwnNamespaceInstallSupport + @WebhookProviderCertManager + Scenario: Reject watchNamespace for operator that does not support Single/OwnNamespace install modes + Given ServiceAccount "olm-admin" in test namespace is cluster admin + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-admin + config: + configType: Inline + inline: + watchNamespace: ${TEST_NAMESPACE} + source: + sourceType: Catalog + catalog: + packageName: webhook-operator + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension reports Progressing as False with Reason InvalidConfiguration and Message includes: + """ + invalid ClusterExtension configuration: invalid configuration: unknown field "watchNamespace" + """ + + @BoxcutterRuntime + @ProgressDeadline + Scenario: Report ClusterExtension as not progressing if the rollout does not complete within given timeout + Given min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1 + And min value for ClusterExtensionRevision .spec.progressDeadlineMinutes is set to 1 + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + progressDeadlineMinutes: 1 + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + version: 1.0.3 + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtensionRevision "${NAME}-1" reports Progressing as False with Reason ProgressDeadlineExceeded + And ClusterExtension reports Progressing as False with Reason ProgressDeadlineExceeded and Message: + """ + Revision has not rolled out for 1 minutes. + """ + And ClusterExtension reports Progressing transition between 1 and 2 minutes since its creation + + @BoxcutterRuntime + Scenario: ClusterExtensionRevision is annotated with bundle properties + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + version: 1.2.0 + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + # The annotation key and value come from the bundle's metadata/properties.yaml file + Then ClusterExtensionRevision "${NAME}-1" contains annotation "olm.properties" with value + """ + [{"type":"olm.test-property","value":"some-value"}] + """ + + @BoxcutterRuntime + Scenario: ClusterExtensionRevision is labeled with owner information + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + version: 1.2.0 + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-kind" with value "ClusterExtension" + And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-name" with value "${NAME}" diff --git a/test/e2e/features/metrics.feature b/test/e2e/features/metrics.feature new file mode 100644 index 0000000000..ccb7191982 --- /dev/null +++ b/test/e2e/features/metrics.feature @@ -0,0 +1,15 @@ +Feature: Exposed various metrics + + Background: + Given OLM is available + + Scenario Outline: component exposes metrics + Given ServiceAccount "metrics-reader" in test namespace has permissions to fetch "" metrics + When ServiceAccount "metrics-reader" sends request to "/metrics" endpoint of "" service + Then Prometheus metrics are returned in the response + + Examples: + | component | + | operator-controller | + | catalogd | + \ No newline at end of file diff --git a/test/e2e/features/recover.feature b/test/e2e/features/recover.feature new file mode 100644 index 0000000000..c222756a47 --- /dev/null +++ b/test/e2e/features/recover.feature @@ -0,0 +1,253 @@ +Feature: Recover cluster extension from errors that might occur during its lifetime + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + + Scenario: Restore removed resource + Given ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension is available + And resource "configmap/test-configmap" exists + When resource "configmap/test-configmap" is removed + Then resource "configmap/test-configmap" is eventually restored + + Scenario: Install ClusterExtension after target namespace becomes available + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as True with Reason Retrying + When ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + Then ClusterExtension is available + And ClusterExtension reports Progressing as True with Reason Succeeded + + Scenario: Install ClusterExtension after conflicting resource is removed + Given ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And resource is applied + """ + apiVersion: apps/v1 + kind: Deployment + metadata: + name: test-operator + namespace: ${TEST_NAMESPACE} + spec: + replicas: 1 + selector: + matchLabels: + app: test-operator + template: + metadata: + labels: + app: test-operator + spec: + containers: + - command: + - "sleep" + args: + - "1000" + image: busybox:1.36 + imagePullPolicy: IfNotPresent + name: busybox + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + """ + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as True with Reason Retrying + And ClusterExtension reports Installed as False + When resource "deployment/test-operator" is removed + Then ClusterExtension is available + And ClusterExtension reports Progressing as True with Reason Succeeded + And ClusterExtension reports Installed as True + + @PreflightPermissions + Scenario: ClusterExtension installation succeeds after service account gets the required missing permissions to + manage the bundle's resources + Given ServiceAccount "olm-sa" is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension reports Progressing as True with Reason Retrying and Message includes: + """ + error for resolved bundle "test-operator.1.2.0" with version "1.2.0": creating new Revision: pre-authorization failed: service account requires the following permissions to manage cluster extension: + """ + And ClusterExtension reports Progressing as True with Reason Retrying and Message includes: + """ + Namespace:"" APIGroups:[apiextensions.k8s.io] Resources:[customresourcedefinitions] ResourceNames:[olme2etests.olm.operatorframework.io] Verbs:[delete,get,patch,update] + """ + When ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + Then ClusterExtension is available + And ClusterExtension reports Progressing as True with Reason Succeeded + And ClusterExtension reports Installed as True + + # CATALOG DELETION RESILIENCE SCENARIOS + + Scenario: Auto-healing continues working after catalog deletion + # This test proves that extensions continue to auto-heal (restore deleted resources) even when + # their source catalog is unavailable. We verify this by: + # 1. Deleting the catalog + # 2. Manually deleting a managed resource (configmap) + # 3. Verifying the resource is automatically restored + # + # Why this proves auto-healing works: + # - If the controller stopped reconciling, the configmap would stay deleted + # - Resource restoration is an observable event that PROVES active reconciliation + # - The deployment staying healthy proves the workload continues running + Given ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension is rolled out + And ClusterExtension is available + And resource "deployment/test-operator" is available + And resource "configmap/test-configmap" is available + When ClusterCatalog "test" is deleted + And resource "configmap/test-configmap" is removed + Then resource "configmap/test-configmap" is eventually restored + And resource "deployment/test-operator" is available + + Scenario: Spec changes are allowed when catalog is unavailable + # This test proves that users can modify extension configuration (non-version changes) even when + # the catalog is missing. We verify this by: + # 1. Deleting the catalog + # 2. Changing the preflight configuration in the ClusterExtension spec + # 3. Verifying the controller accepts and reconciles the change successfully + # + # Why this proves spec changes work without catalog: + # - If the controller rejected the change, Progressing would show Retrying or Failed + # - Reconciliation completing (observedGeneration == generation) proves the spec was processed + # - Progressing=Succeeded proves the controller didn't block on missing catalog + # - Extension staying Available proves workload continues running + Given ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension is rolled out + And ClusterExtension is available + And ClusterCatalog "test" is deleted + When ClusterExtension is updated to add preflight config + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + install: + preflight: + crdUpgradeSafety: + enforcement: None + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension latest generation has been reconciled + And ClusterExtension reports Progressing as True with Reason Succeeded + Then ClusterExtension is available + And ClusterExtension reports Installed as True diff --git a/test/e2e/features/status.feature b/test/e2e/features/status.feature new file mode 100644 index 0000000000..5c8a3141d6 --- /dev/null +++ b/test/e2e/features/status.feature @@ -0,0 +1,45 @@ +Feature: Report status of the managed ClusterExtension workload + + As an OLM user, I would like to see reported on ClusterExtension the availability + change of the managed workload. + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + """ + And ClusterExtension is rolled out + And ClusterExtension is available + + @BoxcutterRuntime + Scenario: Report availability change when managed workload is not ready + When resource "deployment/test-operator" reports as not ready + Then ClusterExtension reports Available as False with Reason ProbeFailure + And ClusterExtensionRevision "${NAME}-1" reports Available as False with Reason ProbeFailure + + @BoxcutterRuntime + Scenario: Report availability change when managed workload restores its readiness + Given resource "deployment/test-operator" reports as not ready + And ClusterExtension reports Available as False with Reason ProbeFailure + And ClusterExtensionRevision "${NAME}-1" reports Available as False with Reason ProbeFailure + When resource "deployment/test-operator" reports as ready + Then ClusterExtension is available + And ClusterExtensionRevision "${NAME}-1" reports Available as True with Reason ProbesSucceeded \ No newline at end of file diff --git a/test/e2e/features/uninstall.feature b/test/e2e/features/uninstall.feature new file mode 100644 index 0000000000..e14b8494fe --- /dev/null +++ b/test/e2e/features/uninstall.feature @@ -0,0 +1,42 @@ +Feature: Uninstall ClusterExtension + + As an OLM user I would like to uninstall a cluster extension, + removing all resources previously installed/updated through the extension. + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + And ClusterExtension is rolled out + And ClusterExtension resources are created and labeled + + Scenario: Removing ClusterExtension triggers the extension uninstall, eventually removing all installed resources + When ClusterExtension is removed + Then the ClusterExtension's constituent resources are removed + + Scenario: Removing ClusterExtension resources leads to all installed resources being removed even if the service account is no longer present + When resource "serviceaccount/olm-sa" is removed + # Ensure service account is gone before checking to ensure resources are cleaned up whether the service account + # and its permissions are present on the cluster or not + And resource "serviceaccount/olm-sa" is eventually not found + And ClusterExtension is removed + Then the ClusterExtension's constituent resources are removed diff --git a/test/e2e/features/update.feature b/test/e2e/features/update.feature new file mode 100644 index 0000000000..782820c2fd --- /dev/null +++ b/test/e2e/features/update.feature @@ -0,0 +1,246 @@ +Feature: Update ClusterExtension + + As an OLM user I would like to update a ClusterExtension from a catalog + or get an appropriate information in case of an error. + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + + Scenario: Update to a successor version + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + """ + And ClusterExtension is rolled out + And ClusterExtension is available + When ClusterExtension is updated to version "1.0.1" + Then ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.0.1" is installed in version "1.0.1" + + Scenario: Cannot update extension to non successor version + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + """ + And ClusterExtension is rolled out + And ClusterExtension is available + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.2.0 + """ + Then ClusterExtension reports Progressing as True with Reason Retrying and Message: + """ + error upgrading from currently installed version "1.0.0": no bundles found for package "test" matching version "1.2.0" + """ + + Scenario: Force update to non successor version + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + """ + And ClusterExtension is rolled out + And ClusterExtension is available + When ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.2.0 + upgradeConstraintPolicy: SelfCertified + """ + Then ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + + @catalog-updates + Scenario: Auto update when new version becomes available in the new catalog image ref + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + When ClusterCatalog "test" is updated to version "v2" + Then bundle "test-operator.1.3.0" is installed in version "1.3.0" + + Scenario: Auto update when new version becomes available in the same catalog image ref + Given "test" catalog image version "v1" is also tagged as "latest" + And ClusterCatalog "test" is updated to version "latest" + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And ClusterExtension is rolled out + And ClusterExtension is available + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + When ClusterCatalog "test" image version "v2" is also tagged as "latest" + Then bundle "test-operator.1.3.0" is installed in version "1.3.0" + + @BoxcutterRuntime + Scenario: Each update creates a new revision and resources not present in the new revision are removed from the cluster + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + upgradeConstraintPolicy: SelfCertified + """ + And ClusterExtension is rolled out + And ClusterExtension is available + When ClusterExtension is updated to version "1.2.0" + Then bundle "test-operator.1.2.0" is installed in version "1.2.0" + And ClusterExtension is rolled out + And ClusterExtension is available + And ClusterExtension reports "${NAME}-2" as active revision + And ClusterExtensionRevision "${NAME}-2" reports Progressing as True with Reason Succeeded + And ClusterExtensionRevision "${NAME}-2" reports Available as True with Reason ProbesSucceeded + And ClusterExtensionRevision "${NAME}-1" is archived + # dummy-config map exists only in v1.0.0 - once the v1.0.0 revision is archived, it should be gone from the cluster + And resource "configmap/dummy-configmap" is eventually not found + + @BoxcutterRuntime + Scenario: Report all active revisions on ClusterExtension + Given ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + version: 1.0.0 + upgradeConstraintPolicy: SelfCertified + """ + And ClusterExtension is rolled out + And ClusterExtension is available + When ClusterExtension is updated to version "1.0.2" + Then ClusterExtension reports "${NAME}-1, ${NAME}-2" as active revisions + And ClusterExtensionRevision "${NAME}-2" reports Progressing as True with Reason Succeeded + And ClusterExtensionRevision "${NAME}-2" reports Available as False with Reason ProbeFailure + diff --git a/test/e2e/features_test.go b/test/e2e/features_test.go new file mode 100644 index 0000000000..706c822efa --- /dev/null +++ b/test/e2e/features_test.go @@ -0,0 +1,75 @@ +package e2e + +import ( + "fmt" + "log" + "os" + "testing" + + "github.com/cucumber/godog" + "github.com/cucumber/godog/colors" + "github.com/spf13/pflag" + + testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" + "github.com/operator-framework/operator-controller/test/e2e/steps" +) + +var opts = godog.Options{ + Format: "pretty", + Paths: []string{"features"}, + Output: colors.Colored(os.Stdout), + Concurrency: 1, + NoColors: true, +} + +func init() { + godog.BindCommandLineFlags("godog.", &opts) +} + +func TestMain(m *testing.M) { + // parse CLI arguments + pflag.Parse() + opts.Paths = pflag.Args() + + // run tests + sc := godog.TestSuite{ + TestSuiteInitializer: InitializeSuite, + ScenarioInitializer: InitializeScenario, + Options: &opts, + }.Run() + + if st := m.Run(); st > sc { + sc = st + } + switch sc { + // 0 - success + case 0: + + path := os.Getenv("E2E_SUMMARY_OUTPUT") + if path == "" { + fmt.Println("Note: E2E_SUMMARY_OUTPUT is unset; skipping summary generation") + } else { + if err := testutil.PrintSummary(path); err != nil { + // Fail the run if alerts are found + fmt.Printf("%v", err) + os.Exit(1) + } + } + return + + // 1 - failed + // 2 - command line usage error + // 128 - or higher, os signal related error exit codes + default: + log.Fatalf("non-zero status returned (%d), failed to run feature tests", sc) + } +} + +func InitializeSuite(tc *godog.TestSuiteContext) { + tc.BeforeSuite(steps.BeforeSuite) +} + +func InitializeScenario(sc *godog.ScenarioContext) { + steps.RegisterSteps(sc) + steps.RegisterHooks(sc) +} diff --git a/test/e2e/metrics_test.go b/test/e2e/metrics_test.go deleted file mode 100644 index e1fbb90f39..0000000000 --- a/test/e2e/metrics_test.go +++ /dev/null @@ -1,297 +0,0 @@ -// Package e2e contains end-to-end tests to verify that the metrics endpoints -// for both components. Metrics are exported and accessible by authorized users through -// RBAC and ServiceAccount tokens. -// -// These tests perform the following steps: -// 1. Create a ClusterRoleBinding to grant necessary permissions for accessing metrics. -// 2. Generate a ServiceAccount token for authentication. -// 3. Deploy a curl pod to interact with the metrics endpoint. -// 4. Wait for the curl pod to become ready. -// 5. Execute a curl command from the pod to validate the metrics endpoint. -// 6. Clean up all resources created during the test, such as the ClusterRoleBinding and curl pod. -// -//nolint:gosec -package e2e - -import ( - "bytes" - "context" - "fmt" - "io" - "os/exec" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/rand" - - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" -) - -// TestOperatorControllerMetricsExportedEndpoint verifies that the metrics endpoint for the operator controller -func TestOperatorControllerMetricsExportedEndpoint(t *testing.T) { - client := testutil.FindK8sClient(t) - curlNamespace := createRandomNamespace(t, client) - componentNamespace := getComponentNamespace(t, client, "control-plane=operator-controller-controller-manager") - - config := NewMetricsTestConfig( - client, - curlNamespace, - componentNamespace, - "operator-controller-metrics-reader", - "operator-controller-metrics-binding", - "operator-controller-metrics-reader", - "oper-curl-metrics", - "app.kubernetes.io/name=operator-controller", - operatorControllerMetricsPort, - ) - - config.run(t) -} - -// TestCatalogdMetricsExportedEndpoint verifies that the metrics endpoint for catalogd -func TestCatalogdMetricsExportedEndpoint(t *testing.T) { - client := testutil.FindK8sClient(t) - curlNamespace := createRandomNamespace(t, client) - componentNamespace := getComponentNamespace(t, client, "control-plane=catalogd-controller-manager") - - config := NewMetricsTestConfig( - client, - curlNamespace, - componentNamespace, - "catalogd-metrics-reader", - "catalogd-metrics-binding", - "catalogd-metrics-reader", - "catalogd-curl-metrics", - "app.kubernetes.io/name=catalogd", - catalogdMetricsPort, - ) - - config.run(t) -} - -// MetricsTestConfig holds the necessary configurations for testing metrics endpoints. -type MetricsTestConfig struct { - client string - namespace string - componentNamespace string - clusterRole string - clusterBinding string - serviceAccount string - curlPodName string - componentSelector string - metricsPort int -} - -// NewMetricsTestConfig initializes a new MetricsTestConfig. -func NewMetricsTestConfig(client, namespace, componentNamespace, clusterRole, clusterBinding, serviceAccount, curlPodName, componentSelector string, metricsPort int) *MetricsTestConfig { - return &MetricsTestConfig{ - client: client, - namespace: namespace, - componentNamespace: componentNamespace, - clusterRole: clusterRole, - clusterBinding: clusterBinding, - serviceAccount: serviceAccount, - curlPodName: curlPodName, - componentSelector: componentSelector, - metricsPort: metricsPort, - } -} - -// run will execute all steps of those tests -func (c *MetricsTestConfig) run(t *testing.T) { - defer c.cleanup(t) - - c.createMetricsClusterRoleBinding(t) - token := c.getServiceAccountToken(t) - c.createCurlMetricsPod(t) - c.validate(t, token) -} - -// createMetricsClusterRoleBinding to binding and expose the metrics -func (c *MetricsTestConfig) createMetricsClusterRoleBinding(t *testing.T) { - t.Logf("Creating ClusterRoleBinding %s for %s in namespace %s", c.clusterBinding, c.serviceAccount, c.namespace) - cmd := exec.Command(c.client, "create", "clusterrolebinding", c.clusterBinding, - "--clusterrole="+c.clusterRole, - "--serviceaccount="+c.namespace+":"+c.serviceAccount) - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Error creating ClusterRoleBinding: %s", string(output)) -} - -// getServiceAccountToken return the token requires to have access to the metrics -func (c *MetricsTestConfig) getServiceAccountToken(t *testing.T) string { - t.Logf("Creating ServiceAccount %q in namespace %q", c.serviceAccount, c.namespace) - output, err := exec.Command(c.client, "create", "serviceaccount", c.serviceAccount, "--namespace="+c.namespace).CombinedOutput() - require.NoError(t, err, "Error creating service account: %v", string(output)) - - t.Logf("Generating ServiceAccount token for %q in namespace %q", c.serviceAccount, c.namespace) - cmd := exec.Command(c.client, "create", "token", c.serviceAccount, "--namespace", c.namespace) - tokenOutput, tokenCombinedOutput, err := stdoutAndCombined(cmd) - require.NoError(t, err, "Error creating token: %s", string(tokenCombinedOutput)) - return string(bytes.TrimSpace(tokenOutput)) -} - -// createCurlMetricsPod creates the Pod with curl image to allow check if the metrics are working -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=quay.io/curl/curl:8.15.0", - "--namespace", c.namespace, - "--restart=Never", - "--overrides", `{ - "spec": { - "terminationGradePeriodSeconds": 0, - "containers": [{ - "name": "curl", - "image": "quay.io/curl/curl:8.15.0", - "command": ["sh", "-c", "sleep 3600"], - "securityContext": { - "allowPrivilegeEscalation": false, - "capabilities": {"drop": ["ALL"]}, - "runAsNonRoot": true, - "runAsUser": 1000, - "seccompProfile": {"type": "RuntimeDefault"} - } - }], - "serviceAccountName": "`+c.serviceAccount+`" - } - }`) - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Error creating curl pod: %s", string(output)) -} - -// validate verifies if is possible to access the metrics from all pods -func (c *MetricsTestConfig) validate(t *testing.T, token string) { - t.Log("Waiting for the curl pod to be ready") - waitCmd := exec.Command(c.client, "wait", "--for=condition=Ready", "pod", c.curlPodName, "--namespace", c.namespace, "--timeout=60s") - waitOutput, waitErr := waitCmd.CombinedOutput() - require.NoError(t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput)) - - // Get all pod IPs for the component - podIPs := c.getComponentPodIPs(t) - require.NotEmpty(t, podIPs, "No pod IPs found for component") - t.Logf("Found %d pod(s) to scrape metrics from", len(podIPs)) - - // Validate metrics endpoint for each pod - for i, podIP := range podIPs { - // Build metrics URL with pod FQDN: ..pod.cluster.local - // Convert IP dots to dashes (e.g., 10.244.0.11 -> 10-244-0-11) - podIPDashes := strings.ReplaceAll(podIP, ".", "-") - metricsURL := fmt.Sprintf("https://%s.%s.pod.cluster.local:%d/metrics", podIPDashes, c.componentNamespace, c.metricsPort) - t.Logf("Validating metrics endpoint for pod %d/%d: %s", i+1, len(podIPs), metricsURL) - - curlCmd := exec.Command(c.client, "exec", c.curlPodName, "--namespace", c.namespace, "--", - "curl", "-v", "-k", "-H", "Authorization: Bearer "+token, metricsURL) - output, err := curlCmd.CombinedOutput() - require.NoError(t, err, "Error calling metrics endpoint %s: %s", metricsURL, string(output)) - require.Contains(t, string(output), "200 OK", "Metrics endpoint %s did not return 200 OK", metricsURL) - t.Logf("Successfully scraped metrics from pod %d/%d", i+1, len(podIPs)) - } -} - -// cleanup removes the created resources. Uses a context with timeout to prevent hangs. -func (c *MetricsTestConfig) cleanup(t *testing.T) { - type objDesc struct { - resourceName string - name string - namespace string - } - objects := []objDesc{ - {"clusterrolebinding", c.clusterBinding, ""}, - {"pod", c.curlPodName, c.namespace}, - {"serviceaccount", c.serviceAccount, c.namespace}, - {"namespace", c.namespace, ""}, - } - - t.Log("Cleaning up resources") - for _, obj := range objects { - args := []string{"delete", obj.resourceName, obj.name, "--ignore-not-found=true", "--force"} - if obj.namespace != "" { - args = append(args, "--namespace", obj.namespace) - } - output, err := exec.Command(c.client, args...).CombinedOutput() - require.NoError(t, err, "Error deleting %q %q in namespace %q: %v", obj.resourceName, obj.name, obj.namespace, string(output)) - } - - // Create a context with a 60-second timeout. - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - for _, obj := range objects { - err := waitForDeletion(ctx, c.client, obj.resourceName, obj.name, obj.namespace) - require.NoError(t, err, "Error deleting %q %q in namespace %q", obj.resourceName, obj.name, obj.namespace) - t.Logf("Successfully deleted %q %q in namespace %q", obj.resourceName, obj.name, obj.namespace) - } -} - -// waitForDeletion uses "kubectl wait" to block until the specified resource is deleted -// or until the 60-second timeout is reached. -func waitForDeletion(ctx context.Context, client, resourceType, resourceName, resourceNamespace string) error { - args := []string{"wait", "--for=delete", "--timeout=60s", resourceType, resourceName} - if resourceNamespace != "" { - args = append(args, "--namespace", resourceNamespace) - } - cmd := exec.CommandContext(ctx, client, args...) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error waiting for deletion of %s %s: %v, output: %s", resourceType, resourceName, err, string(output)) - } - return nil -} - -// createRandomNamespace creates a random namespace -func createRandomNamespace(t *testing.T, client string) string { - nsName := fmt.Sprintf("testns-%s", rand.String(8)) - - cmd := exec.Command(client, "create", "namespace", nsName) - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Error creating namespace: %s", string(output)) - - return nsName -} - -// getComponentNamespace returns the namespace where operator-controller or catalogd is running -func getComponentNamespace(t *testing.T, client, selector string) string { - cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector="+selector, "--output=jsonpath={.items[0].metadata.namespace}") - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Error determining namespace: %s", string(output)) - - namespace := string(bytes.TrimSpace(output)) - if namespace == "" { - t.Fatal("No namespace found for selector " + selector) - } - return namespace -} - -// getComponentPodIPs returns the IP addresses of all pods matching the component selector -func (c *MetricsTestConfig) getComponentPodIPs(t *testing.T) []string { - cmd := exec.Command(c.client, "get", "pods", - "--namespace="+c.componentNamespace, - "--selector="+c.componentSelector, - "--output=jsonpath={.items[*].status.podIP}") - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Error getting pod IPs: %s", string(output)) - - podIPsStr := string(bytes.TrimSpace(output)) - if podIPsStr == "" { - return []string{} - } - - // Split space-separated IPs - fields := bytes.Fields([]byte(podIPsStr)) - ips := make([]string, len(fields)) - for i, field := range fields { - ips[i] = string(field) - } - return ips -} - -func stdoutAndCombined(cmd *exec.Cmd) ([]byte, []byte, error) { - var outOnly, outAndErr bytes.Buffer - allWriter := io.MultiWriter(&outOnly, &outAndErr) - cmd.Stdout = allWriter - cmd.Stderr = &outAndErr - err := cmd.Run() - return outOnly.Bytes(), outAndErr.Bytes(), err -} diff --git a/test/e2e/network_policy_test.go b/test/e2e/network_policy_test.go deleted file mode 100644 index 8e0465f41d..0000000000 --- a/test/e2e/network_policy_test.go +++ /dev/null @@ -1,379 +0,0 @@ -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" - - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" -) - -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 := testutil.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 deleted file mode 100644 index 190e786ba8..0000000000 --- a/test/e2e/single_namespace_support_test.go +++ /dev/null @@ -1,412 +0,0 @@ -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" - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" - . "github.com/operator-framework/operator-controller/test/helpers" -) - -const ( - soNsFlag = "SingleOwnNamespaceInstallSupport" -) - -func TestClusterExtensionSingleNamespaceSupport(t *testing.T) { - SkipIfFeatureGateDisabled(t, soNsFlag) - t.Log("Test support for cluster extension config") - defer testutil.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) { - SkipIfFeatureGateDisabled(t, soNsFlag) - t.Log("Test support for cluster extension with OwnNamespace install mode support") - defer testutil.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, "invalid ClusterExtension configuration") - require.Contains(ct, cond.Message, fmt.Sprintf("watchNamespace must be \"%s\"", clusterExtension.Spec.Namespace)) - require.Contains(ct, cond.Message, "OwnNamespace install mode") - }, 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) -} - -func TestClusterExtensionVersionUpdate(t *testing.T) { - SkipIfFeatureGateDisabled(t, soNsFlag) - 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 testutil.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/e2e/steps/hooks.go b/test/e2e/steps/hooks.go new file mode 100644 index 0000000000..7bea8d230f --- /dev/null +++ b/test/e2e/steps/hooks.go @@ -0,0 +1,187 @@ +package steps + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os/exec" + "regexp" + "strconv" + + "github.com/cucumber/godog" + "github.com/go-logr/logr" + "github.com/spf13/pflag" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/component-base/featuregate" + "k8s.io/klog/v2/textlogger" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/features" +) + +type resource struct { + name string + kind string +} + +type scenarioContext struct { + id string + namespace string + clusterExtensionName string + removedResources []unstructured.Unstructured + backGroundCmds []*exec.Cmd + metricsResponse map[string]string + + extensionObjects []client.Object +} + +// GatherClusterExtensionObjects collects all resources related to the ClusterExtension container in +// either their Helm release Secret or ClusterExtensionRevision depending on the applier being used +// and saves them into the context. +func (s *scenarioContext) GatherClusterExtensionObjects() error { + objs, err := listExtensionResources(s.clusterExtensionName) + if err != nil { + return fmt.Errorf("failed to load extension resources into context: %w", err) + } + s.extensionObjects = objs + return nil +} + +// GetClusterExtensionObjects returns the ClusterExtension objects currently saved into the context. +// Will always return nil until GatherClusterExtensionObjects is called +func (s *scenarioContext) GetClusterExtensionObjects() []client.Object { + return s.extensionObjects +} + +type contextKey string + +const ( + scenarioContextKey contextKey = "scenario-context" +) + +var ( + devMode = false + featureGates = map[featuregate.Feature]bool{ + features.WebhookProviderCertManager: true, + features.PreflightPermissions: false, + features.SingleOwnNamespaceInstallSupport: true, + features.SyntheticPermissions: false, + features.WebhookProviderOpenshiftServiceCA: false, + features.HelmChartSupport: false, + features.BoxcutterRuntime: false, + } + logger logr.Logger +) + +func init() { + flagSet := pflag.CommandLine + flagSet.BoolVar(&devMode, "log.debug", false, "print debug log level") +} + +func RegisterHooks(sc *godog.ScenarioContext) { + sc.Before(CheckFeatureTags) + sc.Before(CreateScenarioContext) + + sc.After(ScenarioCleanup) +} + +func BeforeSuite() { + if devMode { + logger = textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(1))) + } else { + logger = textlogger.NewLogger(textlogger.NewConfig()) + } + + raw, err := k8sClient("get", "deployments", "-A", "-l", "app.kubernetes.io/part-of=olm", "-o", "jsonpath={.items}") + if err != nil { + panic(fmt.Errorf("failed to get OLM deployments: %v", err)) + } + dl := []appsv1.Deployment{} + if err := json.Unmarshal([]byte(raw), &dl); err != nil { + panic(fmt.Errorf("failed to unmarshal OLM deployments: %v", err)) + } + var olm *appsv1.Deployment + + for _, d := range dl { + if d.Name == olmDeploymentName { + olm = &d + olmNamespace = d.Namespace + break + } + } + + featureGatePattern := regexp.MustCompile(`--feature-gates=([[:alnum:]]+)=(true|false)`) + for _, c := range olm.Spec.Template.Spec.Containers { + if c.Name == "manager" { + for _, arg := range c.Args { + if matches := featureGatePattern.FindStringSubmatch(arg); matches != nil { + v, err := strconv.ParseBool(matches[2]) + if err != nil { + panic(fmt.Errorf("failed to parse feature gate %q=%q: %v", matches[1], matches[2], err)) + } + featureGates[featuregate.Feature(matches[1])] = v + } + } + } + } + logger.Info(fmt.Sprintf("Enabled feature gates: %v", featureGates)) +} + +func CheckFeatureTags(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + for _, tag := range sc.Tags { + if enabled, found := featureGates[featuregate.Feature(tag.Name[1:])]; found && !enabled { + logger.Info(fmt.Sprintf("Skipping scenario %q because feature gate %q is disabled", sc.Name, tag.Name[1:])) + return ctx, godog.ErrSkip + } + } + return ctx, nil +} + +func CreateScenarioContext(ctx context.Context, sc *godog.Scenario) (context.Context, error) { + scCtx := &scenarioContext{ + id: sc.Id, + namespace: fmt.Sprintf("ns-%s", sc.Id), + clusterExtensionName: fmt.Sprintf("ce-%s", sc.Id), + } + return context.WithValue(ctx, scenarioContextKey, scCtx), nil +} + +func scenarioCtx(ctx context.Context) *scenarioContext { + return ctx.Value(scenarioContextKey).(*scenarioContext) +} + +func stderrOutput(err error) string { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr != nil { + return string(exitErr.Stderr) + } + return "" +} + +func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) { + sc := scenarioCtx(ctx) + for _, bgCmd := range sc.backGroundCmds { + if p := bgCmd.Process; p != nil { + _ = p.Kill() + } + } + if err != nil { + return ctx, err + } + + forDeletion := []resource{} + if sc.clusterExtensionName != "" { + forDeletion = append(forDeletion, resource{name: sc.clusterExtensionName, kind: "clusterextension"}) + } + forDeletion = append(forDeletion, resource{name: sc.namespace, kind: "namespace"}) + go func() { + for _, r := range forDeletion { + if _, err := k8sClient("delete", r.kind, r.name, "--ignore-not-found=true"); err != nil { + logger.Info("Error deleting resource", "name", r.name, "namespace", sc.namespace, "stderr", stderrOutput(err)) + } + } + }() + return ctx, nil +} diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go new file mode 100644 index 0000000000..483514e403 --- /dev/null +++ b/test/e2e/steps/steps.go @@ -0,0 +1,1170 @@ +package steps + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "time" + + "github.com/cucumber/godog" + jsonpatch "github.com/evanphx/json-patch" + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/release" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/sets" + k8sresource "k8s.io/cli-runtime/pkg/resource" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" +) + +const ( + olmDeploymentName = "operator-controller-controller-manager" + timeout = 5 * time.Minute + tick = 1 * time.Second +) + +var ( + olmNamespace = "olmv1-system" + kubeconfigPath string + k8sCli string +) + +func RegisterSteps(sc *godog.ScenarioContext) { + sc.Step(`^OLM is available$`, OLMisAvailable) + sc.Step(`^(?i)bundle "([^"]+)" is installed in version "([^"]+)"$`, BundleInstalled) + + sc.Step(`^(?i)ClusterExtension is applied(?:\s+.*)?$`, ResourceIsApplied) + sc.Step(`^(?i)ClusterExtension is updated to version "([^"]+)"$`, ClusterExtensionVersionUpdate) + sc.Step(`^(?i)ClusterExtension is updated(?:\s+.*)?$`, ResourceIsApplied) + sc.Step(`^(?i)ClusterExtension is available$`, ClusterExtensionIsAvailable) + sc.Step(`^(?i)ClusterExtension is rolled out$`, ClusterExtensionIsRolledOut) + sc.Step(`^(?i)ClusterExtension resources are created and labeled$`, ClusterExtensionResourcesCreatedAndAreLabeled) + sc.Step(`^(?i)ClusterExtension is removed$`, ClusterExtensionIsRemoved) + sc.Step(`^(?i)ClusterExtension (?:latest generation )?has (?:been )?reconciled(?: the latest generation)?$`, ClusterExtensionReconciledLatestGeneration) + sc.Step(`^(?i)the ClusterExtension's constituent resources are removed$`, ClusterExtensionResourcesRemoved) + sc.Step(`^(?i)ClusterExtension reports "([^"]+)" as active revision(s?)$`, ClusterExtensionReportsActiveRevisions) + sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message:$`, ClusterExtensionReportsCondition) + sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message includes:$`, ClusterExtensionReportsConditionWithMessageFragment) + sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ClusterExtensionReportsConditionWithoutMsg) + sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, ClusterExtensionReportsConditionWithoutReason) + sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+)$`, ClusterExtensionRevisionReportsConditionWithoutMsg) + sc.Step(`^(?i)ClusterExtension reports ([[:alnum:]]+) transition between (\d+) and (\d+) minutes since its creation$`, ClusterExtensionReportsConditionTransitionTime) + sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" is archived$`, ClusterExtensionRevisionIsArchived) + sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" contains annotation "([^"]+)" with value$`, ClusterExtensionRevisionHasAnnotationWithValue) + sc.Step(`^(?i)ClusterExtensionRevision "([^"]+)" has label "([^"]+)" with value "([^"]+)"$`, ClusterExtensionRevisionHasLabelWithValue) + + sc.Step(`^(?i)resource "([^"]+)" is installed$`, ResourceAvailable) + sc.Step(`^(?i)resource "([^"]+)" is available$`, ResourceAvailable) + sc.Step(`^(?i)resource "([^"]+)" is removed$`, ResourceRemoved) + sc.Step(`^(?i)resource "([^"]+)" is eventually not found$`, ResourceEventuallyNotFound) + sc.Step(`^(?i)resource "([^"]+)" exists$`, ResourceAvailable) + sc.Step(`^(?i)resource is applied$`, ResourceIsApplied) + sc.Step(`^(?i)resource "deployment/test-operator" reports as (not ready|ready)$`, MarkTestOperatorNotReady) + + sc.Step(`^(?i)resource apply fails with error msg containing "([^"]+)"$`, ResourceApplyFails) + sc.Step(`^(?i)resource "([^"]+)" is eventually restored$`, ResourceRestored) + sc.Step(`^(?i)resource "([^"]+)" matches$`, ResourceMatches) + + sc.Step(`^(?i)ServiceAccount "([^"]*)" with needed permissions is available in test namespace$`, ServiceAccountWithNeededPermissionsIsAvailableInNamespace) + sc.Step(`^(?i)ServiceAccount "([^"]*)" with needed permissions is available in \${TEST_NAMESPACE}$`, ServiceAccountWithNeededPermissionsIsAvailableInNamespace) + sc.Step(`^(?i)ServiceAccount "([^"]*)" is available in \${TEST_NAMESPACE}$`, ServiceAccountIsAvailableInNamespace) + sc.Step(`^(?i)ServiceAccount "([^"]*)" in test namespace is cluster admin$`, ServiceAccountWithClusterAdminPermissionsIsAvailableInNamespace) + sc.Step(`^(?i)ServiceAccount "([^"]+)" in test namespace has permissions to fetch "([^"]+)" metrics$`, ServiceAccountWithFetchMetricsPermissions) + sc.Step(`^(?i)ServiceAccount "([^"]+)" sends request to "([^"]+)" endpoint of "([^"]+)" service$`, SendMetricsRequest) + + sc.Step(`^"([^"]+)" catalog is updated to version "([^"]+)"$`, CatalogIsUpdatedToVersion) + sc.Step(`^(?i)ClusterCatalog "([^"]+)" is updated to version "([^"]+)"$`, CatalogIsUpdatedToVersion) + sc.Step(`^"([^"]+)" catalog serves bundles$`, CatalogServesBundles) + sc.Step(`^(?i)ClusterCatalog "([^"]+)" serves bundles$`, CatalogServesBundles) + sc.Step(`^"([^"]+)" catalog image version "([^"]+)" is also tagged as "([^"]+)"$`, TagCatalogImage) + sc.Step(`^(?i)ClusterCatalog "([^"]+)" image version "([^"]+)" is also tagged as "([^"]+)"$`, TagCatalogImage) + sc.Step(`^(?i)ClusterCatalog "([^"]+)" is deleted$`, CatalogIsDeleted) + + sc.Step(`^(?i)operator "([^"]+)" target namespace is "([^"]+)"$`, OperatorTargetNamespace) + sc.Step(`^(?i)Prometheus metrics are returned in the response$`, PrometheusMetricsAreReturned) + + sc.Step(`^(?i)min value for (ClusterExtension|ClusterExtensionRevision) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue) +} + +func init() { + flagSet := pflag.CommandLine + flagSet.StringVar(&k8sCli, "k8s.cli", "kubectl", "Path to k8s cli") + if v, found := os.LookupEnv("KUBECONFIG"); found { + kubeconfigPath = v + } else { + home, err := os.UserHomeDir() + if err != nil { + panic(fmt.Sprintf("cannot determine user home directory: %v", err)) + } + flagSet.StringVar(&kubeconfigPath, "kubeconfig", filepath.Join(home, ".kube", "config"), "Paths to a kubeconfig. Only required if out-of-cluster.") + } +} + +func k8sClient(args ...string) (string, error) { + cmd := exec.Command(k8sCli, args...) + logger.V(1).Info("Running", "command", strings.Join(cmd.Args, " ")) + cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + b, err := cmd.Output() + if err != nil { + logger.V(1).Info("Failed to run", "command", strings.Join(cmd.Args, " "), "stderr", stderrOutput(err), "error", err) + } + output := string(b) + logger.V(1).Info("Output", "command", strings.Join(cmd.Args, " "), "output", output) + return output, err +} + +func k8scliWithInput(yaml string, args ...string) (string, error) { + cmd := exec.Command(k8sCli, args...) + cmd.Stdin = bytes.NewBufferString(yaml) + cmd.Env = append(os.Environ(), fmt.Sprintf("KUBECONFIG=%s", kubeconfigPath)) + b, err := cmd.Output() + return string(b), err +} + +func OLMisAvailable(ctx context.Context) error { + require.Eventually(godog.T(ctx), func() bool { + v, err := k8sClient("get", "deployment", "-n", olmNamespace, olmDeploymentName, "-o", "jsonpath='{.status.conditions[?(@.type==\"Available\")].status}'") + if err != nil { + return false + } + return v == "'True'" + }, timeout, tick) + return nil +} + +func BundleInstalled(ctx context.Context, name, version string) error { + sc := scenarioCtx(ctx) + waitFor(ctx, func() bool { + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.install.bundle}") + if err != nil { + return false + } + var bundle map[string]interface{} + if err := json.Unmarshal([]byte(v), &bundle); err != nil { + return false + } + return bundle["name"] == name && bundle["version"] == version + }) + return nil +} + +func toUnstructured(yamlContent string) (*unstructured.Unstructured, error) { + var u map[string]any + if err := yaml.Unmarshal([]byte(yamlContent), &u); err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: u}, nil +} + +func substituteScenarioVars(content string, sc *scenarioContext) string { + vars := map[string]string{ + "TEST_NAMESPACE": sc.namespace, + "NAME": sc.clusterExtensionName, + "CATALOG_IMG": "docker-registry.operator-controller-e2e.svc.cluster.local:5000/e2e/test-catalog:v1", + } + if v, found := os.LookupEnv("CATALOG_IMG"); found { + vars["CATALOG_IMG"] = v + } + return templateContent(content, vars) +} + +func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog.DocString) error { + sc := scenarioCtx(ctx) + yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) + _, err := toUnstructured(yamlContent) + if err != nil { + return fmt.Errorf("failed to parse resource yaml: %v", err) + } + waitFor(ctx, func() bool { + _, err := k8scliWithInput(yamlContent, "apply", "-f", "-") + if err == nil { + return false + } + if stdErr := stderrOutput(err); !strings.Contains(stdErr, errMsg) { + return false + } + return true + }) + return nil +} + +func ClusterExtensionVersionUpdate(ctx context.Context, version string) error { + sc := scenarioCtx(ctx) + patch := map[string]any{ + "spec": map[string]any{ + "source": map[string]any{ + "catalog": map[string]any{ + "version": version, + }, + }, + }, + } + pb, err := json.Marshal(patch) + if err != nil { + return err + } + _, err = k8sClient("patch", "clusterextension", sc.clusterExtensionName, "--type", "merge", "-p", string(pb)) + return err +} + +func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error { + sc := scenarioCtx(ctx) + yamlContent := substituteScenarioVars(yamlTemplate.Content, sc) + res, err := toUnstructured(yamlContent) + if err != nil { + return fmt.Errorf("failed to parse resource yaml: %v", err) + } + out, err := k8scliWithInput(yamlContent, "apply", "-f", "-") + if err != nil { + return fmt.Errorf("failed to apply resource %v %w", out, err) + } + if res.GetKind() == "ClusterExtension" { + sc.clusterExtensionName = res.GetName() + } + return nil +} + +func ClusterExtensionIsAvailable(ctx context.Context) error { + sc := scenarioCtx(ctx) + require.Eventually(godog.T(ctx), func() bool { + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.conditions[?(@.type==\"Installed\")].status}") + if err != nil { + return false + } + return v == "True" + }, timeout, tick) + return nil +} + +func ClusterExtensionReconciledLatestGeneration(ctx context.Context) error { + sc := scenarioCtx(ctx) + waitFor(ctx, func() bool { + // Get both generation and observedGeneration in a single kubectl call + output, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, + "-o", "jsonpath={.metadata.generation},{.status.conditions[?(@.type=='Progressing')].observedGeneration}") + if err != nil || output == "" { + return false + } + parts := strings.Split(output, ",") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return false + } + // Both exist and are equal means reconciliation happened + return parts[0] == parts[1] + }) + return nil +} + +func ClusterExtensionIsRolledOut(ctx context.Context) error { + sc := scenarioCtx(ctx) + require.Eventually(godog.T(ctx), func() bool { + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.conditions[?(@.type==\"Progressing\")]}") + if err != nil { + return false + } + + var condition map[string]interface{} + if err := json.Unmarshal([]byte(v), &condition); err != nil { + return false + } + + return condition["status"] == "True" && condition["reason"] == "Succeeded" && condition["type"] == "Progressing" + }, timeout, tick) + + // Save ClusterExtension resources to test context for posterior checks + if err := sc.GatherClusterExtensionObjects(); err != nil { + return err + } + return nil +} + +func ClusterExtensionResourcesCreatedAndAreLabeled(ctx context.Context) error { + sc := scenarioCtx(ctx) + if len(sc.GetClusterExtensionObjects()) == 0 { + return fmt.Errorf("extension objects not found in context") + } + + for _, obj := range sc.extensionObjects { + waitFor(ctx, func() bool { + kind := obj.GetObjectKind().GroupVersionKind().Kind + clusterObj, err := getResource(kind, obj.GetName(), obj.GetNamespace()) + if err != nil { + logger.V(1).Error(err, "error getting resource", "name", obj.GetName(), "namespace", obj.GetNamespace(), "kind", kind) + return false + } + + labels := clusterObj.GetLabels() + if labels == nil { + logger.V(1).Info("no labels found for resource", "name", obj.GetName(), "namespace", obj.GetNamespace(), "kind", kind) + return false + } + + for key, expectedValue := range map[string]string{ + "olm.operatorframework.io/owner-kind": "ClusterExtension", + "olm.operatorframework.io/owner-name": sc.clusterExtensionName, + } { + if labels[key] != expectedValue { + logger.V(1).Info("invalid resource label value", "name", obj.GetName(), "namespace", obj.GetNamespace(), "kind", kind, "label", key, "expected", expectedValue, "actual", labels["olm.operatorframework.io/owner-kind"]) + return false + } + } + return true + }) + } + return nil +} + +func ClusterExtensionIsRemoved(ctx context.Context) error { + sc := scenarioCtx(ctx) + return ResourceRemoved(ctx, fmt.Sprintf("clusterextension/%s", sc.clusterExtensionName)) +} + +func ClusterExtensionResourcesRemoved(ctx context.Context) error { + sc := scenarioCtx(ctx) + if len(sc.GetClusterExtensionObjects()) == 0 { + return fmt.Errorf("extension objects not found in context") + } + for _, obj := range sc.extensionObjects { + if err := ResourceEventuallyNotFound(ctx, fmt.Sprintf("%s/%s", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName())); err != nil { + return err + } + } + return nil +} + +func waitFor(ctx context.Context, conditionFn func() bool) { + require.Eventually(godog.T(ctx), conditionFn, timeout, tick) +} + +type msgMatchFn func(string) bool + +func alwaysMatch(_ string) bool { return true } + +func waitForCondition(ctx context.Context, resourceType, resourceName, conditionType, conditionStatus string, conditionReason *string, msgCmp msgMatchFn) error { + require.Eventually(godog.T(ctx), func() bool { + v, err := k8sClient("get", resourceType, resourceName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType)) + if err != nil { + return false + } + + var condition metav1.Condition + if err := json.Unmarshal([]byte(v), &condition); err != nil { + return false + } + if condition.Status != metav1.ConditionStatus(conditionStatus) { + return false + } + if conditionReason != nil && condition.Reason != *conditionReason { + return false + } + if msgCmp != nil && !msgCmp(condition.Message) { + return false + } + + return true + }, timeout, tick) + return nil +} + +func waitForExtensionCondition(ctx context.Context, conditionType, conditionStatus string, conditionReason *string, msgCmp msgMatchFn) error { + sc := scenarioCtx(ctx) + return waitForCondition(ctx, "clusterextension", sc.clusterExtensionName, conditionType, conditionStatus, conditionReason, msgCmp) +} + +func ClusterExtensionReportsCondition(ctx context.Context, conditionType, conditionStatus, conditionReason string, msg *godog.DocString) error { + msgCmp := alwaysMatch + if msg != nil { + expectedMsg := substituteScenarioVars(strings.Join(strings.Fields(msg.Content), " "), scenarioCtx(ctx)) + msgCmp = func(actual string) bool { + return actual == expectedMsg + } + } + return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, msgCmp) +} + +func ClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { + msgCmp := alwaysMatch + if msgFragment != nil { + expectedMsgFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx)) + msgCmp = func(actualMsg string) bool { + return strings.Contains(actualMsg, expectedMsgFragment) + } + } + return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, msgCmp) +} + +func ClusterExtensionReportsConditionWithoutMsg(ctx context.Context, conditionType, conditionStatus, conditionReason string) error { + return ClusterExtensionReportsCondition(ctx, conditionType, conditionStatus, conditionReason, nil) +} + +func ClusterExtensionReportsConditionWithoutReason(ctx context.Context, conditionType, conditionStatus string) error { + return waitForExtensionCondition(ctx, conditionType, conditionStatus, nil, nil) +} + +func ClusterExtensionReportsConditionTransitionTime(ctx context.Context, conditionType string, minMinutes, maxMinutes int) error { + sc := scenarioCtx(ctx) + t := godog.T(ctx) + + // Get the ClusterExtension's creation timestamp and condition's lastTransitionTime + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", + fmt.Sprintf("jsonpath={.metadata.creationTimestamp},{.status.conditions[?(@.type==\"%s\")].lastTransitionTime}", conditionType)) + require.NoError(t, err) + + parts := strings.Split(v, ",") + require.Len(t, parts, 2, "expected creationTimestamp and lastTransitionTime but got: %s", v) + + creationTimestamp, err := time.Parse(time.RFC3339, parts[0]) + require.NoError(t, err, "failed to parse creationTimestamp") + + lastTransitionTime, err := time.Parse(time.RFC3339, parts[1]) + require.NoError(t, err, "failed to parse lastTransitionTime") + + transitionDuration := lastTransitionTime.Sub(creationTimestamp) + minDuration := time.Duration(minMinutes) * time.Minute + maxDuration := time.Duration(maxMinutes) * time.Minute + + require.GreaterOrEqual(t, transitionDuration, minDuration, + "condition %s transitioned too early: %v since creation (expected >= %v)", conditionType, transitionDuration, minDuration) + require.LessOrEqual(t, transitionDuration, maxDuration, + "condition %s transitioned too late: %v since creation (expected <= %v)", conditionType, transitionDuration, maxDuration) + + return nil +} + +func ClusterExtensionReportsActiveRevisions(ctx context.Context, rawRevisionNames string) error { + sc := scenarioCtx(ctx) + expectedRevisionNames := sets.New[string]() + for _, rev := range strings.Split(rawRevisionNames, ",") { + expectedRevisionNames.Insert(substituteScenarioVars(strings.TrimSpace(rev), sc)) + } + + waitFor(ctx, func() bool { + v, err := k8sClient("get", "clusterextension", sc.clusterExtensionName, "-o", "jsonpath={.status.activeRevisions}") + if err != nil { + return false + } + var activeRevisions []ocv1.RevisionStatus + if err := json.Unmarshal([]byte(v), &activeRevisions); err != nil { + return false + } + activeRevisionsNames := sets.New[string]() + for _, rev := range activeRevisions { + activeRevisionsNames.Insert(rev.Name) + } + return activeRevisionsNames.Equal(expectedRevisionNames) + }) + return nil +} + +func ClusterExtensionRevisionReportsConditionWithoutMsg(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string) error { + return waitForCondition(ctx, "clusterextensionrevision", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, conditionStatus, &conditionReason, nil) +} + +func ClusterExtensionRevisionIsArchived(ctx context.Context, revisionName string) error { + return waitForCondition(ctx, "clusterextensionrevision", substituteScenarioVars(revisionName, scenarioCtx(ctx)), "Progressing", "False", ptr.To("Archived"), nil) +} + +func ClusterExtensionRevisionHasAnnotationWithValue(ctx context.Context, revisionName, annotationKey string, annotationValue *godog.DocString) error { + sc := scenarioCtx(ctx) + revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + expectedValue := "" + if annotationValue != nil { + expectedValue = annotationValue.Content + } + waitFor(ctx, func() bool { + obj, err := getResource("clusterextensionrevision", revisionName, "") + if err != nil { + logger.V(1).Error(err, "failed to get clusterextensionrevision", "name", revisionName) + return false + } + if obj.GetAnnotations() == nil { + return false + } + return obj.GetAnnotations()[annotationKey] == expectedValue + }) + return nil +} + +func ClusterExtensionRevisionHasLabelWithValue(ctx context.Context, revisionName, labelKey, labelValue string) error { + sc := scenarioCtx(ctx) + revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc) + labelValue = substituteScenarioVars(labelValue, sc) + waitFor(ctx, func() bool { + obj, err := getResource("clusterextensionrevision", revisionName, "") + if err != nil { + logger.V(1).Error(err, "failed to get clusterextensionrevision", "name", revisionName) + return false + } + if obj.GetLabels() == nil { + return false + } + return obj.GetLabels()[labelKey] == labelValue + }) + return nil +} + +func ResourceAvailable(ctx context.Context, resource string) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + waitFor(ctx, func() bool { + _, err := k8sClient("get", kind, name, "-n", sc.namespace) + return err == nil + }) + return nil +} + +func ResourceRemoved(ctx context.Context, resource string) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + yaml, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "yaml") + if err != nil { + return err + } + obj, err := toUnstructured(yaml) + if err != nil { + return err + } + sc.removedResources = append(sc.removedResources, *obj) + _, err = k8sClient("delete", kind, name, "-n", sc.namespace) + return err +} + +func ResourceEventuallyNotFound(ctx context.Context, resource string) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + + waitFor(ctx, func() bool { + obj, err := k8sClient("get", kind, name, "-n", sc.namespace, "--ignore-not-found", "-o", "yaml") + return err == nil && strings.TrimSpace(obj) == "" + }) + return nil +} + +func ResourceMatches(ctx context.Context, resource string, requiredContentTemplate *godog.DocString) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + requiredContent, err := toUnstructured(substituteScenarioVars(requiredContentTemplate.Content, sc)) + if err != nil { + return fmt.Errorf("failed to parse required resource yaml: %v", err) + } + waitFor(ctx, func() bool { + objJson, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "json") + if err != nil { + return false + } + obj, err := toUnstructured(objJson) + if err != nil { + return false + } + patch, err := json.Marshal(requiredContent.Object) + if err != nil { + return false + } + updJson, err := jsonpatch.MergePatch([]byte(objJson), patch) + if err != nil { + return false + } + upd, err := toUnstructured(string(updJson)) + if err != nil { + return false + } + + return len(cmp.Diff(upd.Object, obj.Object)) == 0 + }) + return nil +} + +func ResourceRestored(ctx context.Context, resource string) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + waitFor(ctx, func() bool { + yaml, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "yaml") + if err != nil { + return false + } + obj, err := toUnstructured(yaml) + if err != nil { + return false + } + ct := obj.GetCreationTimestamp() + + for i, removed := range sc.removedResources { + rct := removed.GetCreationTimestamp() + if removed.GetName() == obj.GetName() && removed.GetKind() == obj.GetKind() && rct.Before(&ct) { + switch kind { + case "configmap": + if !reflect.DeepEqual(removed.Object["data"], obj.Object["data"]) { + return false + } + default: + if !reflect.DeepEqual(removed.Object["spec"], obj.Object["spec"]) { + return false + } + } + sc.removedResources = append(sc.removedResources[:i], sc.removedResources[i+1:]...) + return true + } + } + return false + }) + return nil +} + +func applyServiceAccount(ctx context.Context, serviceAccount string) error { + sc := scenarioCtx(ctx) + vars := extendMap(map[string]string{ + "TEST_NAMESPACE": sc.namespace, + "SERVICE_ACCOUNT_NAME": serviceAccount, + "SERVICEACCOUNT_NAME": serviceAccount, + }) + + yaml, err := templateYaml(filepath.Join("steps", "testdata", "serviceaccount-template.yaml"), vars) + if err != nil { + return fmt.Errorf("failed to template ServiceAccount yaml: %v", err) + } + + // Apply the ServiceAccount configuration + _, err = k8scliWithInput(yaml, "apply", "-f", "-") + if err != nil { + return fmt.Errorf("failed to apply ServiceAccount configuration: %v: %s", err, stderrOutput(err)) + } + + return nil +} + +func applyPermissionsToServiceAccount(ctx context.Context, serviceAccount, rbacTemplate string, keyValue ...string) error { + sc := scenarioCtx(ctx) + if err := applyServiceAccount(ctx, serviceAccount); err != nil { + return err + } + vars := extendMap(map[string]string{ + "TEST_NAMESPACE": sc.namespace, + "SERVICE_ACCOUNT_NAME": serviceAccount, + "SERVICEACCOUNT_NAME": serviceAccount, + "CLUSTER_EXTENSION_NAME": sc.clusterExtensionName, + "CLUSTEREXTENSION_NAME": sc.clusterExtensionName, + }, keyValue...) + + yaml, err := templateYaml(filepath.Join("steps", "testdata", rbacTemplate), vars) + if err != nil { + return fmt.Errorf("failed to template RBAC yaml: %v", err) + } + + // Apply the RBAC configuration + _, err = k8scliWithInput(yaml, "apply", "-f", "-") + if err != nil { + return fmt.Errorf("failed to apply RBAC configuration: %v: %s", err, stderrOutput(err)) + } + + return nil +} + +func ServiceAccountIsAvailableInNamespace(ctx context.Context, serviceAccount string) error { + return applyServiceAccount(ctx, serviceAccount) +} + +func ServiceAccountWithNeededPermissionsIsAvailableInNamespace(ctx context.Context, serviceAccount string) error { + return applyPermissionsToServiceAccount(ctx, serviceAccount, "rbac-template.yaml") +} + +func ServiceAccountWithClusterAdminPermissionsIsAvailableInNamespace(ctx context.Context, serviceAccount string) error { + return applyPermissionsToServiceAccount(ctx, serviceAccount, "cluster-admin-rbac-template.yaml") +} + +func ServiceAccountWithFetchMetricsPermissions(ctx context.Context, serviceAccount string, controllerName string) error { + return applyPermissionsToServiceAccount(ctx, serviceAccount, "metrics-reader-rbac-template.yaml", "CONTROLLER_NAME", controllerName) +} + +func httpGet(url string, token string) (*http.Response, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // we don't care about the certificate + } + client := &http.Client{Transport: tr} + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} + +func randomAvailablePort() (int, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func SendMetricsRequest(ctx context.Context, serviceAccount string, endpoint string, controllerName string) error { + sc := scenarioCtx(ctx) + serviceNs, err := k8sClient("get", "service", "-A", "-o", fmt.Sprintf(`jsonpath={.items[?(@.metadata.name=="%s-service")].metadata.namespace}`, controllerName)) + if err != nil { + return err + } + v, err := k8sClient("get", "service", "-n", serviceNs, fmt.Sprintf("%s-service", controllerName), "-o", "json") + if err != nil { + return err + } + var service corev1.Service + if err := json.Unmarshal([]byte(v), &service); err != nil { + return err + } + podNameCmd := []string{"get", "pod", "-n", olmNamespace, "-o", "jsonpath={.items}"} + for k, v := range service.Spec.Selector { + podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v)) + } + v, err = k8sClient(podNameCmd...) + if err != nil { + return err + } + + var pods []corev1.Pod + if err := json.Unmarshal([]byte(v), &pods); err != nil { + return err + } + token, err := k8sClient("create", "token", serviceAccount, "-n", sc.namespace) + if err != nil { + return err + } + var metricsPort int32 + for _, p := range service.Spec.Ports { + if p.Name == "metrics" { + metricsPort = p.Port + break + } + } + sc.metricsResponse = make(map[string]string) + for _, p := range pods { + port, err := randomAvailablePort() + if err != nil { + return err + } + portForwardCmd := exec.Command(k8sCli, "port-forward", "-n", p.Namespace, fmt.Sprintf("pod/%s", p.Name), fmt.Sprintf("%d:%d", port, metricsPort)) //nolint:gosec // perfectly safe to start port-forwarder for provided controller name + logger.V(1).Info("starting port-forward", "command", strings.Join(portForwardCmd.Args, " ")) + if err := portForwardCmd.Start(); err != nil { + logger.Error(err, fmt.Sprintf("failed to start port-forward for pod %s", p.Name)) + return err + } + waitFor(ctx, func() bool { + resp, err := httpGet(fmt.Sprintf("https://localhost:%d%s", port, endpoint), token) + if err != nil { + return false + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + b, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + sc.metricsResponse[p.Name] = string(b) + return true + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + logger.V(1).Info("failed to get metrics", "pod", p.Name, "response", string(b)) + return false + }) + if err := portForwardCmd.Process.Kill(); err != nil { + return err + } + if _, err := portForwardCmd.Process.Wait(); err != nil { + return err + } + } + + return nil +} + +func CatalogIsUpdatedToVersion(name, version string) error { + ref, err := k8sClient("get", "clustercatalog", fmt.Sprintf("%s-catalog", name), "-o", "jsonpath={.spec.source.image.ref}") + if err != nil { + return err + } + i := strings.LastIndexByte(ref, ':') + if i == -1 { + return fmt.Errorf("failed to find tag in image reference %s", ref) + } + base := ref[:i] + patch := map[string]any{ + "spec": map[string]any{ + "source": map[string]any{ + "image": map[string]any{ + "ref": fmt.Sprintf("%s:%s", base, version), + }, + }, + }, + } + pb, err := json.Marshal(patch) + if err != nil { + return err + } + _, err = k8sClient("patch", "clustercatalog", fmt.Sprintf("%s-catalog", name), "--type", "merge", "-p", string(pb)) + return err +} + +func CatalogServesBundles(ctx context.Context, catalogName string) error { + yamlContent, err := os.ReadFile(filepath.Join("steps", "testdata", fmt.Sprintf("%s-catalog-template.yaml", catalogName))) + if err != nil { + return fmt.Errorf("failed to read catalog yaml: %v", err) + } + + _, err = k8scliWithInput(substituteScenarioVars(string(yamlContent), scenarioCtx(ctx)), "apply", "-f", "-") + if err != nil { + return fmt.Errorf("failed to apply catalog: %v", err) + } + + return nil +} + +func TagCatalogImage(name, oldTag, newTag string) error { + imageRef := fmt.Sprintf("%s/%s", os.Getenv("LOCAL_REGISTRY_HOST"), fmt.Sprintf("e2e/%s-catalog:%s", name, oldTag)) + return crane.Tag(imageRef, newTag, crane.Insecure) +} + +func CatalogIsDeleted(ctx context.Context, catalogName string) error { + catalogFullName := fmt.Sprintf("%s-catalog", catalogName) + _, err := k8sClient("delete", "clustercatalog", catalogFullName, "--ignore-not-found=true", "--wait=true") + if err != nil { + return fmt.Errorf("failed to delete catalog: %v", err) + } + return nil +} + +func PrometheusMetricsAreReturned(ctx context.Context) error { + sc := scenarioCtx(ctx) + for podName, mr := range sc.metricsResponse { + if mr == "" { + return fmt.Errorf("metrics response is empty for pod %s", podName) + } + parser := expfmt.NewTextParser(model.UTF8Validation) + metricsFamilies, err := parser.TextToMetricFamilies(strings.NewReader(mr)) + if err != nil { + return fmt.Errorf("failed to parse metrics response for pod %s: %v", podName, err) + } + if len(metricsFamilies) == 0 { + return fmt.Errorf("metrics response does not contain any metrics for pod %s", podName) + } + } + return nil +} + +func OperatorTargetNamespace(ctx context.Context, operator, namespace string) error { + sc := scenarioCtx(ctx) + namespace = substituteScenarioVars(namespace, sc) + raw, err := k8sClient("get", "deployment", "-n", sc.namespace, operator, "-o", "json") + if err != nil { + return err + } + d := &appsv1.Deployment{} + if err := json.Unmarshal([]byte(raw), d); err != nil { + return err + } + + if tns := d.Spec.Template.Annotations["olm.targetNamespaces"]; tns != namespace { + return fmt.Errorf("expected target namespace %s, got %s", namespace, tns) + } + return nil +} + +func MarkTestOperatorNotReady(ctx context.Context, state string) error { + sc := scenarioCtx(ctx) + v, err := k8sClient("get", "deployment", "-n", sc.namespace, "test-operator", "-o", "jsonpath={.spec.selector.matchLabels}") + if err != nil { + return err + } + var labels map[string]string + if err := json.Unmarshal([]byte(v), &labels); err != nil { + return err + } + podNameCmd := []string{"get", "pod", "-n", sc.namespace, "-o", "jsonpath={.items[0].metadata.name}"} + for k, v := range labels { + podNameCmd = append(podNameCmd, fmt.Sprintf("--selector=%s=%s", k, v)) + } + podName, err := k8sClient(podNameCmd...) + if err != nil { + return err + } + var op string + switch state { + case "not ready": + op = "rm" + case "ready": + op = "touch" + default: + return fmt.Errorf("invalid state %s", state) + } + _, err = k8sClient("exec", podName, "-n", sc.namespace, "--", op, "/var/www/ready") + return err +} + +// SetCRDFieldMinValue patches a CRD to set the minimum value for a field. +// jsonPath is in the format ".spec.fieldName" and gets converted to the CRD schema path. +func SetCRDFieldMinValue(_ context.Context, resourceType, jsonPath string, minValue int) error { + var crdName string + switch resourceType { + case "ClusterExtension": + crdName = "clusterextensions.olm.operatorframework.io" + case "ClusterExtensionRevision": + crdName = "clusterextensionrevisions.olm.operatorframework.io" + default: + return fmt.Errorf("unsupported resource type: %s", resourceType) + } + + // Convert JSON path like ".spec.progressDeadlineMinutes" to CRD schema path + // e.g., ".spec.progressDeadlineMinutes" -> "properties/spec/properties/progressDeadlineMinutes" + parts := strings.Split(strings.TrimPrefix(jsonPath, "."), ".") + schemaParts := make([]string, 0, 2*len(parts)) + for _, part := range parts { + schemaParts = append(schemaParts, "properties", part) + } + patchPath := fmt.Sprintf("/spec/versions/0/schema/openAPIV3Schema/%s/minimum", strings.Join(schemaParts, "/")) + + patch := fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %d}]`, patchPath, minValue) + _, err := k8sClient("patch", "crd", crdName, "--type=json", "-p", patch) + return err +} + +// templateYaml applies values to the template located in templatePath and returns the result or any errors reading +// the template file +func templateYaml(templatePath string, values map[string]string) (string, error) { + yamlContent, err := os.ReadFile(templatePath) + if err != nil { + return "", fmt.Errorf("failed to read template file '%s': %v", templatePath, err) + } + return templateContent(string(yamlContent), values), nil +} + +// templateContent applies values to content and returns the result +func templateContent(content string, values map[string]string) string { + m := func(k string) string { + if v, found := values[k]; found { + return v + } + return "" + } + + // Replace template variables + return os.Expand(content, m) +} + +// extendMap extends m with the key/values in keyValue, which is expected to be of even size +func extendMap(m map[string]string, keyValue ...string) map[string]string { + if len(keyValue) > 0 { + for i := 0; i < len(keyValue); i += 2 { + m[keyValue[i]] = keyValue[i+1] + } + } + return m +} + +func getResource(kind string, name string, namespace string) (*unstructured.Unstructured, error) { + out, err := k8sClient("get", kind, name, "-n", namespace, "-o", "yaml") + if err != nil { + return nil, err + } + obj, err := toUnstructured(out) + if err != nil { + return nil, err + } + return obj, nil +} + +// listExtensionResources returns a slice of client.Object containing all resources for a ClusterExtension +// this method is best called when the extension has been installed successfully. An error is returned if there was +// any issue in determining the extension's resources. +func listExtensionResources(extName string) ([]client.Object, error) { + if enabled, found := featureGates[features.BoxcutterRuntime]; found && enabled { + return listExtensionRevisionResources(extName) + } + return listHelmReleaseResources(extName) +} + +// listHelmReleaseResources returns a slice of client.Object containing all resources for a ClusterExtension's +// Helm release. Note: The current implementation does not support release secrets chunked across multiple secrets +func listHelmReleaseResources(extName string) ([]client.Object, error) { + secret, err := helmReleaseSecretForExtension(extName) + if err != nil { + return nil, fmt.Errorf("failed to get helm release secret for extension %s: %w", extName, err) + } + + rel, err := helmReleaseFromSecret(secret) + if err != nil { + return nil, fmt.Errorf("failed to get helm release from secret for cluster extension '%s': %w", extName, err) + } + + objs, err := collectHelmReleaseObjects(rel) + if err != nil { + return nil, fmt.Errorf("failed to collect helm release objects for cluster extension '%s': %w", extName, err) + } + return objs, nil +} + +// helmReleaseSecretForExtension returns the Helm release secret for the extension with name extName +func helmReleaseSecretForExtension(extName string) (*corev1.Secret, error) { + out, err := k8sClient("get", "secrets", "-n", olmNamespace, + "-l", fmt.Sprintf("name=%s,status=deployed", extName), + "--field-selector", "type=operatorframework.io/index.v1", "-o", "json") + if err != nil { + return nil, err + } + if strings.TrimSpace(out) == "" { + return nil, err + } + + var secretList corev1.SecretList + if err = json.Unmarshal([]byte(out), &secretList); err != nil { + return nil, err + } + if len(secretList.Items) != 1 { + return nil, err + } + return &secretList.Items[0], nil +} + +// helmReleaseFromSecret returns the Helm Release object encoded in the secret. Note: this function does not yet support +// releases chunked over multiple Secrets +func helmReleaseFromSecret(secret *corev1.Secret) (*release.Release, error) { + // OLM uses a custom release backend that compresses the release data + gzReader, err := gzip.NewReader(strings.NewReader(string(secret.Data["chunk"]))) + if err != nil { + return nil, err + } + defer gzReader.Close() + + releaseJsonBytes, err := io.ReadAll(gzReader) + if err != nil { + return nil, err + } + + var rel release.Release + if err = json.Unmarshal(releaseJsonBytes, &rel); err != nil { + return nil, err + } + return &rel, nil +} + +// collectHelmReleaseObjects returns a slice of client.Object containing the manifests in rel +func collectHelmReleaseObjects(rel *release.Release) ([]client.Object, error) { + result := k8sresource.NewLocalBuilder().Flatten().Unstructured().Stream(strings.NewReader(rel.Manifest), rel.Name).Do() + if err := result.Err(); err != nil { + return nil, err + } + infos, err := result.Infos() + if err != nil { + return nil, err + } + + objs := make([]client.Object, 0, len(infos)) + for _, info := range infos { + clientObject, ok := info.Object.(client.Object) + if !ok { + return nil, fmt.Errorf("object of type %T does not implement client.Object", info.Object) + } + objs = append(objs, clientObject) + } + return objs, nil +} + +// listExtensionRevisionResources lists objects in the phases of the latest active revision +func listExtensionRevisionResources(extName string) ([]client.Object, error) { + rev, err := latestActiveRevisionForExtension(extName) + if err != nil { + return nil, fmt.Errorf("failed to get latest active revision for extension %s: %w", extName, err) + } + + var objs []client.Object + for i := range rev.Spec.Phases { + phase := &rev.Spec.Phases[i] + for j := range phase.Objects { + objs = append(objs, &phase.Objects[j].Object) + } + } + + return objs, nil +} + +// latestActiveRevisionForExtension returns the latest active revision for the extension called extName +func latestActiveRevisionForExtension(extName string) (*ocv1.ClusterExtensionRevision, error) { + out, err := k8sClient("get", "clusterextensionrevisions", "-l", fmt.Sprintf("olm.operatorframework.io/owner-name=%s", extName), "-o", "json") + if err != nil { + return nil, fmt.Errorf("error listing revisions for extension '%s': %w", extName, err) + } + if strings.TrimSpace(out) == "" { + return nil, fmt.Errorf("no revisions found for extension '%s'", extName) + } + var revisionList ocv1.ClusterExtensionRevisionList + if err := json.Unmarshal([]byte(out), &revisionList); err != nil { + return nil, fmt.Errorf("error unmarshalling revisions for extension '%s': %w", extName, err) + } + + var latest *ocv1.ClusterExtensionRevision + for i := range revisionList.Items { + rev := &revisionList.Items[i] + if rev.Spec.LifecycleState != ocv1.ClusterExtensionRevisionLifecycleStateActive { + continue + } + if latest == nil || rev.Spec.Revision > latest.Spec.Revision { + latest = rev + } + } + + if latest == nil { + return nil, fmt.Errorf("no active revisions found for extension '%s'", extName) + } + + return latest, nil +} diff --git a/test/e2e/steps/testdata/cluster-admin-rbac-template.yaml b/test/e2e/steps/testdata/cluster-admin-rbac-template.yaml new file mode 100644 index 0000000000..c020c7ca55 --- /dev/null +++ b/test/e2e/steps/testdata/cluster-admin-rbac-template.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ${TEST_NAMESPACE} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ${TEST_NAMESPACE}-${SERVICEACCOUNT_NAME}-cluster-admin-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} diff --git a/test/e2e/steps/testdata/extra-catalog-template.yaml b/test/e2e/steps/testdata/extra-catalog-template.yaml new file mode 100644 index 0000000000..a43d9b3246 --- /dev/null +++ b/test/e2e/steps/testdata/extra-catalog-template.yaml @@ -0,0 +1,11 @@ +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: extra-catalog +spec: + priority: 0 + source: + type: Image + image: + pollIntervalMinutes: 1 + ref: ${CATALOG_IMG} diff --git a/test/e2e/steps/testdata/metrics-reader-rbac-template.yaml b/test/e2e/steps/testdata/metrics-reader-rbac-template.yaml new file mode 100644 index 0000000000..4001f86812 --- /dev/null +++ b/test/e2e/steps/testdata/metrics-reader-rbac-template.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ${TEST_NAMESPACE} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ${CONTROLLER_NAME}-metrics-reader-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ${CONTROLLER_NAME}-metrics-reader +subjects: + - kind: ServiceAccount + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} diff --git a/test/e2e/steps/testdata/rbac-template.yaml b/test/e2e/steps/testdata/rbac-template.yaml new file mode 100644 index 0000000000..90138b9c69 --- /dev/null +++ b/test/e2e/steps/testdata/rbac-template.yaml @@ -0,0 +1,68 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ${TEST_NAMESPACE} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ${TEST_NAMESPACE}-${SERVICEACCOUNT_NAME}-olm-admin-clusterrole +rules: + - apiGroups: [olm.operatorframework.io] + resources: [clusterextensions, clusterextensions/finalizers] + resourceNames: ["${CLUSTEREXTENSION_NAME}"] + verbs: [update] + # Allow ClusterExtensionRevisions to set blockOwnerDeletion ownerReferences + - apiGroups: [olm.operatorframework.io] + resources: [clusterextensionrevisions, clusterextensionrevisions/finalizers] + verbs: [update, create, list, watch, get, delete, patch] + - apiGroups: [apiextensions.k8s.io] + resources: [customresourcedefinitions] + verbs: [update, create, list, watch, get, delete, patch] + - apiGroups: [""] + resources: + - configmaps + - secrets + - services + - serviceaccounts + - events + - namespaces + verbs: [update, create, list, watch, get, delete, patch] + - apiGroups: ["apps"] + resources: + - deployments + verbs: [ update, create, list, watch, get, delete, patch ] + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + verbs: [ update, create, list, watch, get, delete, patch ] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - clusterroles + - roles + - clusterrolebindings + - rolebindings + verbs: [ update, create, list, watch, get, delete, patch ] + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: [ update, create, list, watch, get, delete, patch ] + - apiGroups: ["authorization.k8s.io"] + resources: ["subjectaccessreviews"] + verbs: [create] + - apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: [create] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ${TEST_NAMESPACE}-${SERVICEACCOUNT_NAME}-install-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ${TEST_NAMESPACE}-${SERVICEACCOUNT_NAME}-olm-admin-clusterrole +subjects: + - kind: ServiceAccount + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} diff --git a/test/e2e/steps/testdata/serviceaccount-template.yaml b/test/e2e/steps/testdata/serviceaccount-template.yaml new file mode 100644 index 0000000000..5d1c464493 --- /dev/null +++ b/test/e2e/steps/testdata/serviceaccount-template.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ${TEST_NAMESPACE} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${SERVICEACCOUNT_NAME} + namespace: ${TEST_NAMESPACE} \ No newline at end of file diff --git a/test/e2e/steps/testdata/test-catalog-template.yaml b/test/e2e/steps/testdata/test-catalog-template.yaml new file mode 100644 index 0000000000..7e46872f38 --- /dev/null +++ b/test/e2e/steps/testdata/test-catalog-template.yaml @@ -0,0 +1,11 @@ +apiVersion: olm.operatorframework.io/v1 +kind: ClusterCatalog +metadata: + name: test-catalog +spec: + priority: 0 + source: + type: Image + image: + pollIntervalMinutes: 1 + ref: ${CATALOG_IMG} diff --git a/test/e2e/webhook_support_test.go b/test/e2e/webhook_support_test.go deleted file mode 100644 index 9fd05184a6..0000000000 --- a/test/e2e/webhook_support_test.go +++ /dev/null @@ -1,237 +0,0 @@ -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" - testutil "github.com/operator-framework/operator-controller/internal/shared/util/test" - . "github.com/operator-framework/operator-controller/test/helpers" -) - -var dynamicClient dynamic.Interface - -func TestWebhookSupport(t *testing.T) { - SkipIfFeatureGateDisabled(t, "WebhookProviderCertManager") - t.Log("Test support for bundles with webhooks") - defer testutil.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/utils.go b/test/utils.go index 22a50b2b8d..60cfaa38a0 100644 --- a/test/utils.go +++ b/test/utils.go @@ -14,6 +14,7 @@ func NewEnv() *envtest.Environment { testEnv := &envtest.Environment{ CRDDirectoryPaths: []string{ pathFromProjectRoot("helm/olmv1/base/operator-controller/crd/experimental"), + pathFromProjectRoot("helm/olmv1/base/catalogd/crd/experimental"), }, ErrorIfCRDPathMissing: true, } diff --git a/testdata/images/bundles/test-operator/v1.0.0/manifests/dummy.configmap.yaml b/testdata/images/bundles/test-operator/v1.0.0/manifests/dummy.configmap.yaml new file mode 100644 index 0000000000..8135b6f178 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.0/manifests/dummy.configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dummy-configmap +data: + why: "this config map does not exist in higher versions of the bundle - it is useful to test whether resources removed between versions are removed from the cluster as well" diff --git a/testdata/images/bundles/test-operator/v1.0.3/manifests/bundle.configmap.yaml b/testdata/images/bundles/test-operator/v1.0.3/manifests/bundle.configmap.yaml new file mode 100644 index 0000000000..43b73e2c70 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.3/manifests/bundle.configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: wrong-test-configmap +# need such config map with a wrong field so that we can reach progression deadline timeouts +wrongfield: + name: "test-configmap" diff --git a/testdata/images/bundles/test-operator/v1.0.3/manifests/testoperator.clusterserviceversion.yaml b/testdata/images/bundles/test-operator/v1.0.3/manifests/testoperator.clusterserviceversion.yaml new file mode 100644 index 0000000000..a33f5a6c65 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.3/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.0.2 + 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 with a wrong image ref + 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.0.2 + name: test-operator + spec: + replicas: 1 + selector: + matchLabels: + app: olme2etest + template: + metadata: + labels: + app: olme2etest + spec: + terminationGracePeriodSeconds: 0 + volumes: + - name: scripts + configMap: + name: httpd-script + defaultMode: 0755 + containers: + - name: busybox-httpd-container + # This image ref is wrong and should trigger ImagePullBackOff condition + image: busybox:1.36 + 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.0.2 diff --git a/testdata/images/bundles/test-operator/v1.0.3/metadata/annotations.yaml b/testdata/images/bundles/test-operator/v1.0.3/metadata/annotations.yaml new file mode 100644 index 0000000000..404f0f4a34 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.0.3/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: test + operators.operatorframework.io.bundle.channels.v1: beta + 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.2.0/metadata/properties.yaml b/testdata/images/bundles/test-operator/v1.2.0/metadata/properties.yaml new file mode 100644 index 0000000000..3a2bfb2a64 --- /dev/null +++ b/testdata/images/bundles/test-operator/v1.2.0/metadata/properties.yaml @@ -0,0 +1,3 @@ +properties: + - type: olm.test-property + value: "some-value" diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml index 111c75f42c..012afbe830 100644 --- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml @@ -8,6 +8,7 @@ package: test entries: - name: test-operator.1.0.0 - name: test-operator.1.0.2 + - name: test-operator.1.0.3 --- schema: olm.channel name: beta @@ -50,6 +51,17 @@ properties: packageName: test version: 1.0.2 --- +# Bundle with an invalid config map ensure that we can never successfully rollout - used to test progression deadline timeouts +schema: olm.bundle +name: test-operator.1.0.3 +package: test +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/test-operator:v1.0.3 +properties: + - type: olm.package + value: + packageName: test + version: 1.0.3 +--- schema: olm.bundle name: test-operator.1.2.0 package: test