1
0
mirror of https://github.com/rancher/steve.git synced 2025-07-16 16:01:37 +00:00

Implement /ext in Steve for Imperative API (#287)

This implements the Imperative API that is served at /ext with Steve. The imperative API is compatible with Kubernetes' API server and will be used as an extension API server.
This commit is contained in:
Tom Lebreux 2024-10-11 15:19:27 -04:00 committed by GitHub
parent 57a25ffa82
commit 1f21e5e515
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 5343 additions and 4 deletions

View File

@ -25,6 +25,8 @@ jobs:
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
with: with:
version: v1.59.0 version: v1.59.0
- name: Install env-test
run: go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
- name: Build - name: Build
run: make build-bin run: make build-bin
- name: Test - name: Test

View File

@ -795,3 +795,21 @@ Integration tests for the steve API are located among the [rancher integration
tests](ihttps://github.com/rancher/rancher/tree/release/v2.8/tests/v2/integration/steveapi). tests](ihttps://github.com/rancher/rancher/tree/release/v2.8/tests/v2/integration/steveapi).
See the documentation included there for running the tests and using them to See the documentation included there for running the tests and using them to
generate API documentation. generate API documentation.
## Running Tests
Some of steve's tests make use of [envtest](https://book.kubebuilder.io/reference/envtest) to run. Envtest allows tests to run against a "fake" kubernetes server with little/no overhead.
To install the required `setup-envtest` binary, use the following command:
```bash
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
```
Before running the tests, you must run the following command to setup the fake server:
```bash
# note that this will use a new/latest version of k8s. Our CI will run against the version of k8s that corresponds to steve's
# current client-go version, as seen in scripts/test.sh
export KUBEBUILDER_ASSETS=$(setup-envtest use -p path)
```

23
go.mod
View File

@ -40,19 +40,27 @@ require (
k8s.io/klog v1.0.0 k8s.io/klog v1.0.0
k8s.io/kube-aggregator v0.31.1 k8s.io/kube-aggregator v0.31.1
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
sigs.k8s.io/controller-runtime v0.19.0
) )
require ( require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
@ -61,13 +69,17 @@ require (
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/imdario/mergo v0.3.13 // indirect github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
@ -83,9 +95,15 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
@ -94,7 +112,10 @@ require (
go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.26.0 // indirect golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/net v0.28.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.23.0 // indirect golang.org/x/sys v0.23.0 // indirect
@ -107,9 +128,11 @@ require (
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.31.1 // indirect k8s.io/component-base v0.31.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kms v0.31.1 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect modernc.org/libc v1.49.3 // indirect

80
go.sum
View File

@ -11,10 +11,16 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
@ -24,6 +30,10 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@ -43,9 +53,13 @@ github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 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.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -57,6 +71,8 @@ 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.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 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/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.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@ -70,11 +86,16 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 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-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/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.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.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
@ -86,6 +107,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -114,6 +139,12 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -124,6 +155,10 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -209,15 +244,22 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
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/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -225,17 +267,39 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 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/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
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 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.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
@ -256,6 +320,10 @@ 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -264,6 +332,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -339,6 +409,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -346,6 +418,8 @@ google.golang.org/appengine v1.5.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-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-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-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
@ -364,6 +438,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -398,6 +474,8 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.31.1 h1:cGLyV3cIwb0ovpP/jtyIe2mEuQ/MkbhmeBF2IYCA9Io=
k8s.io/kms v0.31.1/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
k8s.io/kube-aggregator v0.31.1 h1:vrYBTTs3xMrpiEsmBjsLETZE9uuX67oQ8B3i1BFfMPw= k8s.io/kube-aggregator v0.31.1 h1:vrYBTTs3xMrpiEsmBjsLETZE9uuX67oQ8B3i1BFfMPw=
k8s.io/kube-aggregator v0.31.1/go.mod h1:+aW4NX50uneozN+BtoCxI4g7ND922p8Wy3tWKFDiWVk= k8s.io/kube-aggregator v0.31.1/go.mod h1:+aW4NX50uneozN+BtoCxI4g7ND922p8Wy3tWKFDiWVk=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
@ -436,6 +514,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsA
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg=
sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco=
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 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-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=

277
pkg/ext/apiserver.go Normal file
View File

@ -0,0 +1,277 @@
package ext
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/openapi"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
utilversion "k8s.io/apiserver/pkg/util/version"
openapicommon "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var (
schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, metainternalversion.AddToScheme)
AddToScheme = schemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
return nil
}
type ExtensionAPIServerOptions struct {
// GetOpenAPIDefinitions is collection of all definitions. Required.
GetOpenAPIDefinitions openapicommon.GetOpenAPIDefinitions
OpenAPIDefinitionNameReplacements map[string]string
// Authenticator will be used to authenticate requests coming to the
// extension API server. Required.
Authenticator authenticator.Request
// Authorizer will be used to authorize requests based on the user,
// operation and resources. Required.
//
// Use [NewAccessSetAuthorizer] for an authorizer that uses Steve's access set.
Authorizer authorizer.Authorizer
// Listener is the TCP listener that is used to listen to the extension API server
// that is reached by the main kube-apiserver. Required.
Listener net.Listener
// EffectiveVersion determines which features and apis are supported
// by our custom API server.
//
// This is a new alpha feature from Kubernetes, the details can be
// found here: https://github.com/kubernetes/enhancements/tree/master/keps/sig-architecture/4330-compatibility-versions
//
// If nil, the default version is the version of the Kubernetes Go library
// compiled in the final binary.
EffectiveVersion utilversion.EffectiveVersion
}
// ExtensionAPIServer wraps a [genericapiserver.GenericAPIServer] to implement
// a Kubernetes extension API server.
//
// Use [NewExtensionAPIServer] to create an ExtensionAPIServer.
//
// Use [InstallStore] to add a new resource store onto an existing ExtensionAPIServer.
// Each resources will then be reachable via /apis/<group>/<version>/<resource> as
// defined by the Kubernetes API.
//
// When Run() is called, a separate HTTPS server is started. This server is meant
// for the main kube-apiserver to communicate with our extension API server. We
// can expect the following requests from the main kube-apiserver:
//
// <path> <user> <groups>
// /openapi/v2 system:aggregator [system:authenticated]
// /openapi/v3 system:aggregator [system:authenticated]
// /apis system:kube-aggregator [system:masters system:authenticated]
// /apis/ext.cattle.io/v1 system:kube-aggregator [system:masters system:authenticated]
type ExtensionAPIServer struct {
codecs serializer.CodecFactory
scheme *runtime.Scheme
genericAPIServer *genericapiserver.GenericAPIServer
apiGroups map[string]genericapiserver.APIGroupInfo
authorizer authorizer.Authorizer
handlerMu sync.RWMutex
handler http.Handler
}
type emptyAddresses struct{}
func (e emptyAddresses) ServerAddressByClientCIDRs(clientIP net.IP) []metav1.ServerAddressByClientCIDR {
return nil
}
func NewExtensionAPIServer(scheme *runtime.Scheme, codecs serializer.CodecFactory, opts ExtensionAPIServerOptions) (*ExtensionAPIServer, error) {
if opts.Authenticator == nil {
return nil, fmt.Errorf("authenticator must be provided")
}
if opts.Authorizer == nil {
return nil, fmt.Errorf("authorizer must be provided")
}
if opts.Listener == nil {
return nil, fmt.Errorf("listener must be provided")
}
recommendedOpts := genericoptions.NewRecommendedOptions("", codecs.LegacyCodec())
recommendedOpts.SecureServing.Listener = opts.Listener
resolver := &request.RequestInfoFactory{APIPrefixes: sets.NewString("apis", "api"), GrouplessAPIPrefixes: sets.NewString("api")}
config := genericapiserver.NewRecommendedConfig(codecs)
config.RequestInfoResolver = resolver
config.Authorization = genericapiserver.AuthorizationInfo{
Authorizer: opts.Authorizer,
}
// The default kube effective version ends up being the version of the
// library. (The value is hardcoded but it is kept up-to-date via some
// automation)
config.EffectiveVersion = utilversion.DefaultKubeEffectiveVersion()
if opts.EffectiveVersion != nil {
config.EffectiveVersion = opts.EffectiveVersion
}
// This feature is more of an optimization for clients that want to go directly to a custom API server
// instead of going through the main apiserver. We currently don't need to support this so we're leaving this
// empty.
config.DiscoveryAddresses = emptyAddresses{}
config.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(opts.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(scheme))
config.OpenAPIConfig.Info.Title = "Ext"
config.OpenAPIConfig.Info.Version = "0.1"
config.OpenAPIConfig.GetDefinitionName = getDefinitionName(scheme, opts.OpenAPIDefinitionNameReplacements)
// Must set to nil otherwise getDefinitionName won't be used for refs
// which will break kubectl explain
config.OpenAPIConfig.Definitions = nil
config.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(opts.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(scheme))
config.OpenAPIV3Config.Info.Title = "Ext"
config.OpenAPIV3Config.Info.Version = "0.1"
config.OpenAPIV3Config.GetDefinitionName = getDefinitionName(scheme, opts.OpenAPIDefinitionNameReplacements)
// Must set to nil otherwise getDefinitionName won't be used for refs
// which will break kubectl explain
config.OpenAPIV3Config.Definitions = nil
if err := recommendedOpts.SecureServing.ApplyTo(&config.SecureServing, &config.LoopbackClientConfig); err != nil {
return nil, fmt.Errorf("applyto secureserving: %w", err)
}
config.Authentication.Authenticator = opts.Authenticator
completedConfig := config.Complete()
genericServer, err := completedConfig.New("imperative-api", genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, fmt.Errorf("new: %w", err)
}
extensionAPIServer := &ExtensionAPIServer{
codecs: codecs,
scheme: scheme,
genericAPIServer: genericServer,
apiGroups: make(map[string]genericapiserver.APIGroupInfo),
authorizer: opts.Authorizer,
}
return extensionAPIServer, nil
}
// Run prepares and runs the separate HTTPS server. It also configures the handler
// so that ServeHTTP can be used.
func (s *ExtensionAPIServer) Run(ctx context.Context) error {
for _, apiGroup := range s.apiGroups {
err := s.genericAPIServer.InstallAPIGroup(&apiGroup)
if err != nil {
return fmt.Errorf("installgroup: %w", err)
}
}
prepared := s.genericAPIServer.PrepareRun()
s.handlerMu.Lock()
s.handler = prepared.Handler
s.handlerMu.Unlock()
return nil
}
func (s *ExtensionAPIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.handlerMu.RLock()
defer s.handlerMu.RUnlock()
s.handler.ServeHTTP(w, req)
}
// InstallStore installs a store on the given ExtensionAPIServer object.
//
// t and TList must be non-nil.
//
// Here's an example store for a Token and TokenList resource in the ext.cattle.io/v1 apiVersion:
//
// gvk := schema.GroupVersionKind{
// Group: "ext.cattle.io",
// Version: "v1",
// Kind: "Token",
// }
// InstallStore(s, &Token{}, &TokenList{}, "tokens", "token", gvk, store)
//
// Note: Not using a method on ExtensionAPIServer object due to Go generic limitations.
func InstallStore[T runtime.Object, TList runtime.Object](
s *ExtensionAPIServer,
t T,
tList TList,
resourceName string,
singularName string,
gvk schema.GroupVersionKind,
store Store[T, TList],
) error {
if !meta.IsListType(tList) {
return fmt.Errorf("tList (%T) is not a list type", tList)
}
apiGroup, ok := s.apiGroups[gvk.Group]
if !ok {
apiGroup = genericapiserver.NewDefaultAPIGroupInfo(gvk.Group, s.scheme, metav1.ParameterCodec, s.codecs)
}
_, ok = apiGroup.VersionedResourcesStorageMap[gvk.Version]
if !ok {
apiGroup.VersionedResourcesStorageMap[gvk.Version] = make(map[string]rest.Storage)
}
delegate := &delegate[T, TList]{
scheme: s.scheme,
t: t,
tList: tList,
singularName: singularName,
gvk: gvk,
gvr: schema.GroupVersionResource{
Group: gvk.Group,
Version: gvk.Version,
Resource: resourceName,
},
authorizer: s.authorizer,
store: store,
}
apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = delegate
s.apiGroups[gvk.Group] = apiGroup
return nil
}
func getDefinitionName(scheme *runtime.Scheme, replacements map[string]string) func(string) (string, spec.Extensions) {
return func(name string) (string, spec.Extensions) {
namer := openapi.NewDefinitionNamer(scheme)
definitionName, defGVK := namer.GetDefinitionName(name)
for key, val := range replacements {
if !strings.HasPrefix(definitionName, key) {
continue
}
updatedName := strings.ReplaceAll(definitionName, key, val)
return updatedName, defGVK
}
return definitionName, defGVK
}
}

View File

@ -0,0 +1,170 @@
package ext
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server/options"
)
type authnTestStore struct {
*testStore
userCh chan user.Info
}
func (t *authnTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
t.userCh <- ctx.User
return &testTypeListFixture, nil
}
func (t *authnTestStore) getUser() (user.Info, bool) {
timer := time.NewTimer(time.Second * 5)
defer timer.Stop()
select {
case user := <-t.userCh:
return user, true
case <-timer.C:
return nil, false
}
}
func TestAuthenticationCustom(t *testing.T) {
scheme := runtime.NewScheme()
AddToScheme(scheme)
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
require.NoError(t, err)
store := &authnTestStore{
testStore: &testStore{},
userCh: make(chan user.Info, 100),
}
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
opts.Listener = ln
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
user, ok := request.UserFrom(req.Context())
if !ok {
return nil, false, nil
}
if user.GetName() == "error" {
return nil, false, fmt.Errorf("fake error")
}
return &authenticator.Response{
User: user,
}, true, nil
})
}, nil)
require.NoError(t, err)
defer cleanup()
unauthorized := apierrors.NewUnauthorized("Unauthorized")
unauthorized.ErrStatus.Kind = "Status"
unauthorized.ErrStatus.APIVersion = "v1"
allPaths := []string{
"/",
"/apis",
"/apis/ext.cattle.io",
"/apis/ext.cattle.io/v1",
"/apis/ext.cattle.io/v1/testtypes",
"/apis/ext.cattle.io/v1/testtypes/foo",
"/openapi/v2",
"/openapi/v3",
"/openapi/v3/apis/ext.cattle.io/v1",
}
tests := []struct {
name string
user *user.DefaultInfo
paths []string
expectedStatusCode int
expectedStatus apierrors.APIStatus
expectedUser *user.DefaultInfo
}{
{
name: "authenticated request check user",
paths: []string{"/apis/ext.cattle.io/v1/testtypes"},
user: &user.DefaultInfo{Name: "my-user", Groups: []string{"my-group", "system:authenticated"}, Extra: map[string][]string{}},
expectedStatusCode: http.StatusOK,
expectedUser: &user.DefaultInfo{Name: "my-user", Groups: []string{"my-group", "system:authenticated"}, Extra: map[string][]string{}},
},
{
name: "authenticated request all paths",
user: &user.DefaultInfo{Name: "my-user", Groups: []string{"my-group", "system:authenticated"}, Extra: map[string][]string{}},
paths: allPaths,
expectedStatusCode: http.StatusOK,
},
{
name: "authenticated request to unknown endpoint",
user: &user.DefaultInfo{Name: "my-user", Groups: []string{"my-group", "system:authenticated"}, Extra: map[string][]string{}},
paths: []string{"/unknown"},
expectedStatusCode: http.StatusNotFound,
},
{
name: "unauthenticated request",
paths: append(allPaths, "/unknown"),
expectedStatusCode: http.StatusUnauthorized,
expectedStatus: unauthorized,
},
{
name: "authentication error",
user: &user.DefaultInfo{Name: "error"},
paths: append(allPaths, "/unknown"),
expectedStatusCode: http.StatusUnauthorized,
expectedStatus: unauthorized,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, path := range test.paths {
req := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
if test.user != nil {
ctx := request.WithUser(req.Context(), test.user)
req = req.WithContext(ctx)
}
extensionAPIServer.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
responseStatus := metav1.Status{}
json.Unmarshal(body, &responseStatus)
require.Equal(t, test.expectedStatusCode, resp.StatusCode, "for path "+path)
if test.expectedStatus != nil {
require.Equal(t, test.expectedStatus.Status(), responseStatus, "for path "+path)
}
if test.expectedUser != nil {
authUser, found := store.getUser()
require.True(t, found)
require.Equal(t, test.expectedUser, authUser)
}
}
})
}
}

View File

@ -0,0 +1,47 @@
package ext
import (
"context"
"github.com/rancher/steve/pkg/accesscontrol"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ authorizer.Authorizer = (*AccessSetAuthorizer)(nil)
type AccessSetAuthorizer struct {
asl accesscontrol.AccessSetLookup
}
func NewAccessSetAuthorizer(asl accesscontrol.AccessSetLookup) *AccessSetAuthorizer {
return &AccessSetAuthorizer{
asl: asl,
}
}
// Authorize implements [authorizer.Authorizer].
func (a *AccessSetAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if !attrs.IsResourceRequest() {
// XXX: Implement
return authorizer.DecisionDeny, "AccessSetAuthorizer does not support nonResourceURLs requests", nil
}
verb := attrs.GetVerb()
namespace := attrs.GetNamespace()
name := attrs.GetName()
gr := schema.GroupResource{
Group: attrs.GetAPIGroup(),
Resource: attrs.GetResource(),
}
accessSet := a.asl.AccessFor(attrs.GetUser())
if accessSet.Grants(verb, gr, namespace, name) {
return authorizer.DecisionAllow, "", nil
}
// An empty string reason will still provide enough information such as:
//
// testtypes.ext.cattle.io is forbidden: User "unknown-user" cannot list resource "testtypes" in API group "ext.cattle.io" at the cluster scope
return authorizer.DecisionDeny, "", nil
}

View File

@ -0,0 +1,344 @@
package ext
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/rancher/lasso/pkg/controller"
"github.com/rancher/steve/pkg/accesscontrol"
wrbacv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/rbac/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/server/options"
)
type authzTestStore struct {
*testStore
}
func (t *authzTestStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
if name == "not-found" {
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
}
return t.testStore.Get(ctx, name, opts)
}
func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
if ctx.User.GetName() == "read-only-error" {
decision, _, err := ctx.Authorizer.Authorize(ctx, authorizer.AttributesRecord{
User: ctx.User,
Verb: "customverb",
Resource: "testtypes",
ResourceRequest: true,
APIGroup: "ext.cattle.io",
})
if err != nil || decision != authorizer.DecisionAllow {
if err == nil {
err = fmt.Errorf("not allowed")
}
forbidden := apierrors.NewForbidden(ctx.GroupVersionResource.GroupResource(), "Forbidden", err)
forbidden.ErrStatus.Kind = "Status"
forbidden.ErrStatus.APIVersion = "v1"
return nil, forbidden
}
}
return &testTypeListFixture, nil
}
func (s *ExtensionAPIServerSuite) TestAuthorization() {
t := s.T()
scheme := runtime.NewScheme()
AddToScheme(scheme)
rbacv1.AddToScheme(scheme)
codecs := serializer.NewCodecFactory(scheme)
controllerFactory, err := controller.NewSharedControllerFactoryFromConfigWithOptions(s.restConfig, scheme, &controller.SharedControllerFactoryOptions{})
require.NoError(t, err)
rbacController := wrbacv1.New(controllerFactory)
accessStore := accesscontrol.NewAccessStore(s.ctx, false, rbacController)
authz := NewAccessSetAuthorizer(accessStore)
err = controllerFactory.Start(s.ctx, 2)
require.NoError(t, err)
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
require.NoError(t, err)
store := &authzTestStore{
testStore: &testStore{},
}
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
opts.Listener = ln
opts.Authorizer = authz
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
user, ok := request.UserFrom(req.Context())
if !ok {
return nil, false, nil
}
return &authenticator.Response{
User: user,
}, true, nil
})
}, nil)
require.NoError(t, err)
defer cleanup()
rbacBytes, err := os.ReadFile(filepath.Join("testdata", "rbac.yaml"))
require.NoError(t, err)
decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(rbacBytes), 4096)
for {
var rawObj runtime.RawExtension
if err = decoder.Decode(&rawObj); err != nil {
break
}
obj, _, err := codecs.UniversalDecoder(rbacv1.SchemeGroupVersion).Decode(rawObj.Raw, nil, nil)
require.NoError(t, err)
switch obj := obj.(type) {
case *rbacv1.ClusterRole:
_, err = s.client.RbacV1().ClusterRoles().Create(s.ctx, obj, metav1.CreateOptions{})
defer func(name string) {
s.client.RbacV1().ClusterRoles().Delete(s.ctx, obj.GetName(), metav1.DeleteOptions{})
}(obj.GetName())
case *rbacv1.ClusterRoleBinding:
_, err = s.client.RbacV1().ClusterRoleBindings().Create(s.ctx, obj, metav1.CreateOptions{})
defer func(name string) {
s.client.RbacV1().ClusterRoleBindings().Delete(s.ctx, obj.GetName(), metav1.DeleteOptions{})
}(obj.GetName())
}
require.NoError(t, err, "creating")
}
tests := []struct {
name string
user *user.DefaultInfo
createRequest func() *http.Request
expectedStatusCode int
expectedStatus apierrors.APIStatus
}{
{
name: "authorized get read-only not found",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/not-found", nil)
},
expectedStatusCode: http.StatusNotFound,
},
{
name: "authorized get read-only",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
},
expectedStatusCode: http.StatusOK,
},
{
name: "authorized list read-only",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
},
expectedStatusCode: http.StatusOK,
},
{
name: "unauthorized create from read-only",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodPost, "/apis/ext.cattle.io/v1/testtypes", nil)
},
expectedStatusCode: http.StatusForbidden,
},
{
name: "unauthorized update from read-only",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
},
expectedStatusCode: http.StatusForbidden,
},
{
name: "unauthorized delete from read-only",
user: &user.DefaultInfo{
Name: "read-only",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodDelete, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
},
expectedStatusCode: http.StatusForbidden,
},
{
name: "unauthorized create-on-update",
user: &user.DefaultInfo{
Name: "update-not-create",
},
createRequest: func() *http.Request {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(&TestType{
TypeMeta: metav1.TypeMeta{
Kind: "TestType",
APIVersion: testTypeGV.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "not-found",
},
})
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/not-found", &buf)
},
expectedStatusCode: http.StatusForbidden,
},
{
name: "authorized read-only-error with custom store authorization",
user: &user.DefaultInfo{
Name: "read-only-error",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
},
expectedStatusCode: http.StatusForbidden,
},
{
name: "authorized get read-write not found",
user: &user.DefaultInfo{
Name: "read-write",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/not-found", nil)
},
expectedStatusCode: http.StatusNotFound,
},
{
name: "authorized get read-write",
user: &user.DefaultInfo{
Name: "read-write",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil)
},
expectedStatusCode: http.StatusOK,
},
{
name: "authorized list read-write",
user: &user.DefaultInfo{
Name: "read-write",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
},
expectedStatusCode: http.StatusOK,
},
{
name: "authorized create from read-write",
user: &user.DefaultInfo{
Name: "read-write",
},
createRequest: func() *http.Request {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(&TestType{
TypeMeta: metav1.TypeMeta{
Kind: "TestType",
APIVersion: testTypeGV.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
})
return httptest.NewRequest(http.MethodPost, "/apis/ext.cattle.io/v1/testtypes", &buf)
},
expectedStatusCode: http.StatusCreated,
},
{
name: "authorized update from read-write",
user: &user.DefaultInfo{
Name: "read-write",
},
createRequest: func() *http.Request {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(&TestType{
TypeMeta: metav1.TypeMeta{
Kind: "TestType",
APIVersion: testTypeGV.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
})
return httptest.NewRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", &buf)
},
expectedStatusCode: http.StatusOK,
},
{
name: "unauthorized user",
user: &user.DefaultInfo{
Name: "unknown-user",
},
createRequest: func() *http.Request {
return httptest.NewRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil)
},
expectedStatusCode: http.StatusForbidden,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := test.createRequest()
w := httptest.NewRecorder()
if test.user != nil {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
accessSet := accessStore.AccessFor(test.user)
assert.NotNil(c, accessSet)
}, time.Second*5, 100*time.Millisecond)
ctx := request.WithUser(req.Context(), test.user)
req = req.WithContext(ctx)
}
extensionAPIServer.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
responseStatus := metav1.Status{}
json.Unmarshal(body, &responseStatus)
require.Equal(t, test.expectedStatusCode, resp.StatusCode)
if test.expectedStatus != nil {
require.Equal(t, test.expectedStatus.Status(), responseStatus, "for request "+req.URL.String())
}
})
}
}

View File

@ -0,0 +1,50 @@
package ext
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
type ExtensionAPIServerSuite struct {
suite.Suite
ctx context.Context
cancel context.CancelFunc
testEnv envtest.Environment
client *kubernetes.Clientset
restConfig *rest.Config
}
func (s *ExtensionAPIServerSuite) SetupSuite() {
var err error
apiServer := &envtest.APIServer{}
s.testEnv = envtest.Environment{
ControlPlane: envtest.ControlPlane{
APIServer: apiServer,
},
}
s.restConfig, err = s.testEnv.Start()
s.Require().NoError(err)
s.client, err = kubernetes.NewForConfig(s.restConfig)
s.Require().NoError(err)
s.ctx, s.cancel = context.WithCancel(context.Background())
}
func (s *ExtensionAPIServerSuite) TearDownSuite() {
s.cancel()
err := s.testEnv.Stop()
s.Require().NoError(err)
}
func TestExtensionAPIServerSuite(t *testing.T) {
suite.Run(t, new(ExtensionAPIServerSuite))
}

770
pkg/ext/apiserver_test.go Normal file
View File

@ -0,0 +1,770 @@
package ext
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)
func authAsAdmin(req *http.Request) (*authenticator.Response, bool, error) {
return &authenticator.Response{
User: &user.DefaultInfo{
Name: "system:masters",
Groups: []string{"system:masters"},
},
}, true, nil
}
func authzAllowAll(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return authorizer.DecisionAllow, "", nil
}
type mapStore struct {
items map[string]*TestType
events chan WatchEvent[*TestType]
}
func newMapStore() *mapStore {
return &mapStore{
items: make(map[string]*TestType),
events: make(chan WatchEvent[*TestType], 100),
}
}
func (t *mapStore) Create(ctx Context, obj *TestType, opts *metav1.CreateOptions) (*TestType, error) {
if _, found := t.items[obj.Name]; found {
return nil, apierrors.NewAlreadyExists(ctx.GroupVersionResource.GroupResource(), obj.Name)
}
t.items[obj.Name] = obj
t.events <- WatchEvent[*TestType]{
Event: watch.Added,
Object: obj,
}
return obj, nil
}
func (t *mapStore) Update(ctx Context, obj *TestType, opts *metav1.UpdateOptions) (*TestType, error) {
if _, found := t.items[obj.Name]; !found {
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), obj.Name)
}
obj.ManagedFields = []metav1.ManagedFieldsEntry{}
t.items[obj.Name] = obj
t.events <- WatchEvent[*TestType]{
Event: watch.Modified,
Object: obj,
}
return obj, nil
}
func (t *mapStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
obj, found := t.items[name]
if !found {
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
}
return obj, nil
}
func (t *mapStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
items := []TestType{}
for _, obj := range t.items {
items = append(items, *obj)
}
sort.Slice(items, func(i, j int) bool {
return items[i].Name > items[j].Name
})
list := &TestTypeList{
Items: items,
}
return list, nil
}
func (t *mapStore) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestType], error) {
return t.events, nil
}
func (t *mapStore) Delete(ctx Context, name string, opts *metav1.DeleteOptions) error {
obj, found := t.items[name]
if !found {
return apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
}
delete(t.items, name)
t.events <- WatchEvent[*TestType]{
Event: watch.Deleted,
Object: obj,
}
return nil
}
func TestStore(t *testing.T) {
scheme := runtime.NewScheme()
AddToScheme(scheme)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0")
require.NoError(t, err)
store := newMapStore()
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
opts.Listener = ln
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
}, nil)
require.NoError(t, err)
defer cleanup()
ts := httptest.NewServer(extensionAPIServer)
defer ts.Close()
recWatch, err := createRecordingWatcher(scheme, testTypeGV.WithResource("testtypes"), ts.URL)
require.NoError(t, err)
updatedObj := testTypeFixture.DeepCopy()
updatedObj.Annotations = map[string]string{
"foo": "bar",
}
updatedObjList := testTypeListFixture.DeepCopy()
updatedObjList.Items = []TestType{*updatedObj}
emptyList := testTypeListFixture.DeepCopy()
emptyList.Items = []TestType{}
createRequest := func(method string, path string, obj any) *http.Request {
var body io.Reader
if obj != nil {
raw, err := json.Marshal(obj)
require.NoError(t, err)
body = bytes.NewReader(raw)
}
return httptest.NewRequest(method, path, body)
}
tests := []struct {
name string
request *http.Request
newType any
expectedStatusCode int
expectedBody any
}{
{
name: "delete not existing",
request: createRequest(http.MethodDelete, "/apis/ext.cattle.io/v1/testtypes/foo", nil),
expectedStatusCode: http.StatusNotFound,
},
{
name: "get empty list",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil),
newType: &TestTypeList{},
expectedStatusCode: http.StatusOK,
expectedBody: emptyList,
},
{
name: "create testtype",
request: createRequest(http.MethodPost, "/apis/ext.cattle.io/v1/testtypes", testTypeFixture.DeepCopy()),
newType: &TestType{},
expectedStatusCode: http.StatusCreated,
expectedBody: &testTypeFixture,
},
{
name: "get non-empty list",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil),
newType: &TestTypeList{},
expectedStatusCode: http.StatusOK,
expectedBody: &testTypeListFixture,
},
{
name: "get specific object",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil),
newType: &TestType{},
expectedStatusCode: http.StatusOK,
expectedBody: &testTypeFixture,
},
{
name: "update",
request: createRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", updatedObj.DeepCopy()),
newType: &TestType{},
expectedStatusCode: http.StatusOK,
expectedBody: updatedObj,
},
{
name: "get updated",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil),
newType: &TestTypeList{},
expectedStatusCode: http.StatusOK,
expectedBody: updatedObjList,
},
{
name: "delete",
request: createRequest(http.MethodDelete, "/apis/ext.cattle.io/v1/testtypes/foo", nil),
newType: &TestType{},
expectedStatusCode: http.StatusOK,
expectedBody: updatedObj,
},
{
name: "delete not found",
request: createRequest(http.MethodDelete, "/apis/ext.cattle.io/v1/testtypes/foo", nil),
expectedStatusCode: http.StatusNotFound,
},
{
name: "get not found",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes/foo", nil),
expectedStatusCode: http.StatusNotFound,
},
{
name: "get empty list again",
request: createRequest(http.MethodGet, "/apis/ext.cattle.io/v1/testtypes", nil),
newType: &TestTypeList{},
expectedStatusCode: http.StatusOK,
expectedBody: emptyList,
},
{
name: "create via update",
newType: &TestType{},
request: createRequest(http.MethodPut, "/apis/ext.cattle.io/v1/testtypes/foo", testTypeFixture.DeepCopy()),
expectedStatusCode: http.StatusCreated,
expectedBody: &testTypeFixture,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := test.request
w := httptest.NewRecorder()
extensionAPIServer.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
require.Equal(t, test.expectedStatusCode, resp.StatusCode)
if test.expectedBody != nil && test.newType != nil {
err = json.Unmarshal(body, test.newType)
require.NoError(t, err)
require.Equal(t, test.expectedBody, test.newType)
}
})
}
// Possibly flaky, find a better way to wait for all events
time.Sleep(1 * time.Second)
expectedEvents := []watch.Event{
{Type: watch.Added, Object: testTypeFixture.DeepCopy()},
{Type: watch.Modified, Object: updatedObj.DeepCopy()},
{Type: watch.Deleted, Object: updatedObj.DeepCopy()},
{Type: watch.Added, Object: testTypeFixture.DeepCopy()},
}
events := recWatch.getEvents()
require.Equal(t, len(expectedEvents), len(events))
for i, event := range events {
raw, err := json.Marshal(event.Object)
require.NoError(t, err)
obj := &TestType{}
err = json.Unmarshal(raw, obj)
require.NoError(t, err)
convertedEvent := watch.Event{
Type: event.Type,
Object: obj,
}
require.Equal(t, expectedEvents[i], convertedEvent)
}
}
var _ Store[*TestTypeOther, *TestTypeOtherList] = (*testStoreOther)(nil)
// This store is meant to be able to test many stores
type testStoreOther struct {
}
func (t *testStoreOther) Create(ctx Context, obj *TestTypeOther, opts *metav1.CreateOptions) (*TestTypeOther, error) {
return &testTypeOtherFixture, nil
}
func (t *testStoreOther) Update(ctx Context, obj *TestTypeOther, opts *metav1.UpdateOptions) (*TestTypeOther, error) {
return &testTypeOtherFixture, nil
}
func (t *testStoreOther) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestTypeOther, error) {
return &testTypeOtherFixture, nil
}
func (t *testStoreOther) List(ctx Context, opts *metav1.ListOptions) (*TestTypeOtherList, error) {
return &testTypeOtherListFixture, nil
}
func (t *testStoreOther) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestTypeOther], error) {
return nil, nil
}
func (t *testStoreOther) Delete(ctx Context, name string, opts *metav1.DeleteOptions) error {
return nil
}
// The POC had a bug where multiple resources couldn't be installed so we're
// testing this here
func TestDiscoveryAndOpenAPI(t *testing.T) {
scheme := runtime.NewScheme()
AddToScheme(scheme)
differentVersion := schema.GroupVersion{
Group: "ext.cattle.io",
Version: "v2",
}
differentGroupVersion := schema.GroupVersion{
Group: "ext2.cattle.io",
Version: "v3",
}
scheme.AddKnownTypes(differentVersion, &TestType{}, &TestTypeList{})
scheme.AddKnownTypes(differentGroupVersion, &TestType{}, &TestTypeList{})
metav1.AddToGroupVersion(scheme, differentVersion)
metav1.AddToGroupVersion(scheme, differentGroupVersion)
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", ":0")
require.NoError(t, err)
store := &testStore{}
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
opts.Listener = ln
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
}, func(s *ExtensionAPIServer) error {
store := &testStoreOther{}
err := InstallStore(s, &TestTypeOther{}, &TestTypeOtherList{}, "testtypeothers", "testtypeother", testTypeGV.WithKind("TestTypeOther"), store)
if err != nil {
return err
}
err = InstallStore(s, &TestType{}, &TestTypeList{}, "testtypes", "testtype", differentVersion.WithKind("TestType"), &testStore{})
if err != nil {
return err
}
err = InstallStore(s, &TestType{}, &TestTypeList{}, "testtypes", "testtype", differentGroupVersion.WithKind("TestType"), &testStore{})
if err != nil {
return err
}
return nil
})
require.NoError(t, err)
defer cleanup()
tests := []struct {
path string
got any
expectedStatusCode int
expectedBody any
compareFunc func(*testing.T, any)
}{
{
path: "/apis",
got: &metav1.APIGroupList{},
expectedStatusCode: http.StatusOK,
// This is needed because the library loops over the apigroups
compareFunc: func(t *testing.T, gotObj any) {
apiGroupList, ok := gotObj.(*metav1.APIGroupList)
require.True(t, ok)
expectedAPIGroupList := &metav1.APIGroupList{
TypeMeta: metav1.TypeMeta{
Kind: "APIGroupList",
},
Groups: []metav1.APIGroup{
{
Name: "ext.cattle.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "ext.cattle.io/v2",
Version: "v2",
},
{
GroupVersion: "ext.cattle.io/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "ext.cattle.io/v2",
Version: "v2",
},
},
{
Name: "ext2.cattle.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "ext2.cattle.io/v3",
Version: "v3",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "ext2.cattle.io/v3",
Version: "v3",
},
},
},
}
sortAPIGroupList(apiGroupList)
sortAPIGroupList(expectedAPIGroupList)
require.Equal(t, expectedAPIGroupList, apiGroupList)
},
},
{
path: "/apis/ext.cattle.io",
got: &metav1.APIGroup{},
expectedStatusCode: http.StatusOK,
expectedBody: &metav1.APIGroup{
TypeMeta: metav1.TypeMeta{
Kind: "APIGroup",
APIVersion: "v1",
},
Name: "ext.cattle.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "ext.cattle.io/v2",
Version: "v2",
},
{
GroupVersion: "ext.cattle.io/v1",
Version: "v1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "ext.cattle.io/v2",
Version: "v2",
},
},
},
{
path: "/apis/ext2.cattle.io",
got: &metav1.APIGroup{},
expectedStatusCode: http.StatusOK,
expectedBody: &metav1.APIGroup{
TypeMeta: metav1.TypeMeta{
Kind: "APIGroup",
APIVersion: "v1",
},
Name: "ext2.cattle.io",
Versions: []metav1.GroupVersionForDiscovery{
{
GroupVersion: "ext2.cattle.io/v3",
Version: "v3",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "ext2.cattle.io/v3",
Version: "v3",
},
},
},
{
path: "/apis/ext.cattle.io/v1",
got: &metav1.APIResourceList{},
expectedStatusCode: http.StatusOK,
expectedBody: &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "ext.cattle.io/v1",
APIResources: []metav1.APIResource{
{
Name: "testtypeothers",
SingularName: "testtypeother",
Namespaced: false,
Kind: "TestTypeOther",
Group: "ext.cattle.io",
Version: "v1",
Verbs: metav1.Verbs{
"create", "delete", "get", "list", "patch", "update", "watch",
},
},
{
Name: "testtypes",
SingularName: "testtype",
Namespaced: false,
Kind: "TestType",
Group: "ext.cattle.io",
Version: "v1",
Verbs: metav1.Verbs{
"create", "delete", "get", "list", "patch", "update", "watch",
},
},
},
},
},
{
path: "/apis/ext.cattle.io/v2",
got: &metav1.APIResourceList{},
expectedStatusCode: http.StatusOK,
expectedBody: &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "ext.cattle.io/v2",
APIResources: []metav1.APIResource{
{
Name: "testtypes",
SingularName: "testtype",
Namespaced: false,
Kind: "TestType",
Group: "ext.cattle.io",
Version: "v2",
Verbs: metav1.Verbs{
"create", "delete", "get", "list", "patch", "update", "watch",
},
},
},
},
},
{
path: "/apis/ext2.cattle.io/v3",
got: &metav1.APIResourceList{},
expectedStatusCode: http.StatusOK,
expectedBody: &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "ext2.cattle.io/v3",
APIResources: []metav1.APIResource{
{
Name: "testtypes",
SingularName: "testtype",
Namespaced: false,
Kind: "TestType",
Group: "ext2.cattle.io",
Version: "v3",
Verbs: metav1.Verbs{
"create", "delete", "get", "list", "patch", "update", "watch",
},
},
},
},
},
{
path: "/openapi/v2",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis/ext.cattle.io",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis/ext.cattle.io/v1",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis/ext.cattle.io/v2",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis/ext2.cattle.io",
expectedStatusCode: http.StatusOK,
},
{
path: "/openapi/v3/apis/ext2.cattle.io/v3",
expectedStatusCode: http.StatusOK,
},
}
for _, test := range tests {
name := strings.ReplaceAll(test.path, "/", "_")
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, test.path, nil)
w := httptest.NewRecorder()
extensionAPIServer.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
require.Equal(t, test.expectedStatusCode, resp.StatusCode)
if test.expectedBody != nil && test.got != nil {
err = json.Unmarshal(body, test.got)
require.NoError(t, err)
require.Equal(t, test.expectedBody, test.got)
}
if test.got != nil && test.compareFunc != nil {
err = json.Unmarshal(body, test.got)
require.NoError(t, err)
test.compareFunc(t, test.got)
}
})
}
}
// Because the library has non-deterministic map iteration, changing the order of groups and versions
func sortAPIGroupList(list *metav1.APIGroupList) {
for _, group := range list.Groups {
sort.Slice(group.Versions, func(i, j int) bool {
return group.Versions[i].GroupVersion > group.Versions[j].GroupVersion
})
}
sort.Slice(list.Groups, func(i, j int) bool {
return list.Groups[i].Name > list.Groups[j].Name
})
}
func TestNoStore(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", ":0")
require.NoError(t, err)
opts := ExtensionAPIServerOptions{
GetOpenAPIDefinitions: getOpenAPIDefinitions,
Listener: ln,
Authorizer: authorizer.AuthorizerFunc(authzAllowAll),
Authenticator: authenticator.RequestFunc(authAsAdmin),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
extensionAPIServer, err := NewExtensionAPIServer(scheme, codecs, opts)
require.NoError(t, err)
err = extensionAPIServer.Run(ctx)
require.NoError(t, err)
}
func setupExtensionAPIServer[
T runtime.Object,
TList runtime.Object,
](
t *testing.T,
scheme *runtime.Scheme,
objT T,
objTList TList,
store Store[T, TList],
optionSetter func(*ExtensionAPIServerOptions),
extensionAPIServerSetter func(*ExtensionAPIServer) error,
) (*ExtensionAPIServer, func(), error) {
addToSchemeTest(scheme)
codecs := serializer.NewCodecFactory(scheme)
opts := ExtensionAPIServerOptions{
GetOpenAPIDefinitions: getOpenAPIDefinitions,
OpenAPIDefinitionNameReplacements: map[string]string{
"com.github.rancher.steve.pkg.ext": "io.cattle.ext.v1",
},
}
if optionSetter != nil {
optionSetter(&opts)
}
extensionAPIServer, err := NewExtensionAPIServer(scheme, codecs, opts)
if err != nil {
return nil, func() {}, err
}
err = InstallStore(extensionAPIServer, objT, objTList, "testtypes", "testtype", testTypeGV.WithKind("TestType"), store)
if err != nil {
return nil, func() {}, fmt.Errorf("InstallStore: %w", err)
}
if extensionAPIServerSetter != nil {
err = extensionAPIServerSetter(extensionAPIServer)
if err != nil {
return nil, func() {}, fmt.Errorf("extensionAPIServerSetter: %w", err)
}
}
ctx, cancel := context.WithCancel(context.Background())
err = extensionAPIServer.Run(ctx)
require.NoError(t, err)
cleanup := func() {
cancel()
}
return extensionAPIServer, cleanup, nil
}
type recordingWatcher struct {
ch <-chan watch.Event
stop func()
}
func (w *recordingWatcher) getEvents() []watch.Event {
w.stop()
events := []watch.Event{}
for event := range w.ch {
events = append(events, event)
}
return events
}
func createRecordingWatcher(scheme *runtime.Scheme, gvr schema.GroupVersionResource, url string) (*recordingWatcher, error) {
codecs := serializer.NewCodecFactory(scheme)
gv := gvr.GroupVersion()
client, err := dynamic.NewForConfig(&rest.Config{
Host: url,
APIPath: "/apis",
ContentConfig: rest.ContentConfig{
NegotiatedSerializer: codecs,
GroupVersion: &gv,
},
})
if err != nil {
return nil, err
}
opts := metav1.ListOptions{
Watch: true,
}
myWatch, err := client.Resource(gvr).Watch(context.Background(), opts)
if err != nil {
return nil, err
}
// Should be plenty enough for most tests
ch := make(chan watch.Event, 100)
go func() {
for event := range myWatch.ResultChan() {
ch <- event
}
close(ch)
}()
return &recordingWatcher{
ch: ch,
stop: myWatch.Stop,
}, nil
}

351
pkg/ext/delegate.go Normal file
View File

@ -0,0 +1,351 @@
package ext
import (
"context"
"fmt"
"sync"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
// delegate is the bridge between k8s.io/apiserver's [rest.Storage] interface and
// our own Store interface we want developers to use
//
// It currently supports non-namespaced stores only because Store[T, TList] doesn't
// expose namespaces anywhere. When needed we'll add support to namespaced resources.
type delegate[T runtime.Object, TList runtime.Object] struct {
scheme *runtime.Scheme
// t is the resource of the delegate (eg: *Token) and must be non-nil.
t T
// tList is the resource list of the delegate (eg: *TokenList) and must be non-nil.
tList TList
gvk schema.GroupVersionKind
gvr schema.GroupVersionResource
singularName string
store Store[T, TList]
authorizer authorizer.Authorizer
}
// New implements [rest.Storage]
//
// It uses generics to create the resource and set its GVK.
func (s *delegate[T, TList]) New() runtime.Object {
t := s.t.DeepCopyObject()
t.GetObjectKind().SetGroupVersionKind(s.gvk)
return t
}
// Destroy cleans up its resources on shutdown.
// Destroy has to be implemented in thread-safe way and be prepared
// for being called more than once.
//
// It is NOT meant to delete resources from the backing storage. It is meant to
// stop clients, runners, etc that could be running for the store when the extension
// API server gracefully shutdowns/exits.
func (s *delegate[T, TList]) Destroy() {
}
// NewList implements [rest.Lister]
//
// It uses generics to create the resource and set its GVK.
func (s *delegate[T, TList]) NewList() runtime.Object {
tList := s.tList.DeepCopyObject()
tList.GetObjectKind().SetGroupVersionKind(s.gvk)
return tList
}
// List implements [rest.Lister]
func (s *delegate[T, TList]) List(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (runtime.Object, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, err
}
options, err := s.convertListOptions(internaloptions)
if err != nil {
return nil, err
}
return s.store.List(ctx, options)
}
// ConvertToTable implements [rest.Lister]
//
// It converts an object or a list of objects to a table, which is used by kubectl
// (and Rancher UI) to display a table of the items.
//
// Currently, we use the default table convertor which will show two columns: Name and Created At.
func (s *delegate[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
defaultTableConverter := rest.NewDefaultTableConvertor(s.gvr.GroupResource())
return defaultTableConverter.ConvertToTable(ctx, object, tableOptions)
}
// Get implements [rest.Getter]
func (s *delegate[T, TList]) Get(parentCtx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, err
}
return s.store.Get(ctx, name, options)
}
// Delete implements [rest.GracefulDeleter]
//
// deleteValidation is used to do some validation on the object before deleting
// it in the store. For example, running mutating/validating webhooks, though we're not using these yet.
func (s *delegate[T, TList]) Delete(parentCtx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, false, err
}
oldObj, err := s.store.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
if deleteValidation != nil {
if err = deleteValidation(ctx, oldObj); err != nil {
return nil, false, err
}
}
err = s.store.Delete(ctx, name, options)
return oldObj, true, err
}
// Create implements [rest.Creater]
//
// createValidation is used to do some validation on the object before creating
// it in the store. For example, running mutating/validating webhooks, though we're not using these yet.
//
//nolint:misspell
func (s *delegate[T, TList]) Create(parentCtx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, err
}
if createValidation != nil {
err := createValidation(ctx, obj)
if err != nil {
return obj, err
}
}
tObj, ok := obj.(T)
if !ok {
return nil, fmt.Errorf("object was of type %T, not of expected type %T", obj, s.t)
}
return s.store.Create(ctx, tObj, options)
}
// Update implements [rest.Updater]
//
// createValidation is used to do some validation on the object before creating
// it in the store. For example, it will do an authorization check for "create"
// verb if the object needs to be created.
// See here for details: https://github.com/kubernetes/apiserver/blob/70ed6fdbea9eb37bd1d7558e90c20cfe888955e8/pkg/endpoints/handlers/update.go#L190-L201
// Another example is running mutating/validating webhooks, though we're not using these yet.
//
// updateValidation is used to do some validation on the object before updating it in the store.
// One example is running mutating/validating webhooks, though we're not using these yet.
func (s *delegate[T, TList]) Update(
parentCtx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
forceAllowCreate bool,
options *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, false, err
}
oldObj, err := s.store.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
return nil, false, err
}
obj, err := objInfo.UpdatedObject(ctx, nil)
if err != nil {
return nil, false, err
}
if err = createValidation(ctx, obj); err != nil {
return nil, false, err
}
tObj, ok := obj.(T)
if !ok {
return nil, false, fmt.Errorf("object was of type %T, not of expected type %T", obj, s.t)
}
newObj, err := s.store.Create(ctx, tObj, &metav1.CreateOptions{})
if err != nil {
return nil, false, err
}
return newObj, true, err
}
newObj, err := objInfo.UpdatedObject(ctx, oldObj)
if err != nil {
return nil, false, err
}
newT, ok := newObj.(T)
if !ok {
return nil, false, fmt.Errorf("object was of type %T, not of expected type %T", newObj, s.t)
}
if updateValidation != nil {
err = updateValidation(ctx, newT, oldObj)
if err != nil {
return nil, false, err
}
}
newT, err = s.store.Update(ctx, newT, options)
if err != nil {
return nil, false, err
}
return newT, false, nil
}
type watcher struct {
closedLock sync.RWMutex
closed bool
ch chan watch.Event
}
func (w *watcher) Stop() {
w.closedLock.Lock()
defer w.closedLock.Unlock()
if !w.closed {
close(w.ch)
w.closed = true
}
}
func (w *watcher) addEvent(event watch.Event) bool {
w.closedLock.RLock()
defer w.closedLock.RUnlock()
if w.closed {
return false
}
w.ch <- event
return true
}
func (w *watcher) ResultChan() <-chan watch.Event {
return w.ch
}
func (s *delegate[T, TList]) Watch(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) {
ctx, err := s.makeContext(parentCtx)
if err != nil {
return nil, err
}
options, err := s.convertListOptions(internaloptions)
if err != nil {
return nil, err
}
w := &watcher{
ch: make(chan watch.Event),
}
go func() {
// Not much point continuing the watch if the store stopped its watch.
// Double stopping here is fine.
defer w.Stop()
// Closing eventCh is the responsibility of the store.Watch method
// to avoid the store panicking while trying to send to a close channel
eventCh, err := s.store.Watch(ctx, options)
if err != nil {
return
}
for event := range eventCh {
added := w.addEvent(watch.Event{
Type: event.Event,
Object: event.Object,
})
if !added {
break
}
}
}()
return w, nil
}
// GroupVersionKind implements rest.GroupVersionKind
//
// This is used to generate the data for the Discovery API
func (s *delegate[T, TList]) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
return s.gvk
}
// NamespaceScoped implements rest.Scoper
//
// The delegate is used for non-namespaced resources so it always returns false
func (s *delegate[T, TList]) NamespaceScoped() bool {
return false
}
// Kind implements rest.KindProvider
//
// XXX: Example where / how this is used
func (s *delegate[T, TList]) Kind() string {
return s.gvk.Kind
}
// GetSingularName implements rest.SingularNameProvider
//
// This is used by a variety of things such as kubectl to map singular name to
// resource name. (eg: token => tokens)
func (s *delegate[T, TList]) GetSingularName() string {
return s.singularName
}
func (s *delegate[T, TList]) makeContext(parentCtx context.Context) (Context, error) {
userInfo, ok := request.UserFrom(parentCtx)
if !ok {
return Context{}, fmt.Errorf("missing user info")
}
ctx := Context{
Context: parentCtx,
User: userInfo,
Authorizer: s.authorizer,
GroupVersionResource: s.gvr,
}
return ctx, nil
}
func (s *delegate[T, TList]) convertListOptions(options *metainternalversion.ListOptions) (*metav1.ListOptions, error) {
var out metav1.ListOptions
err := s.scheme.Convert(options, &out, nil)
if err != nil {
return nil, fmt.Errorf("convert list options: %w", err)
}
return &out, nil
}

2946
pkg/ext/fixtures_test.go Normal file

File diff suppressed because it is too large Load Diff

93
pkg/ext/store.go Normal file
View File

@ -0,0 +1,93 @@
package ext
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// Context wraps a context.Context and adds a few fields that will be useful for
// each requests handled by a Store.
//
// It will allow us to add more such fields without breaking Store implementation.
type Context struct {
context.Context
// User is the user making the request
User user.Info
// Authorizer helps you determines if a user is authorized to perform
// actions to specific resources.
Authorizer authorizer.Authorizer
// GroupVersionResource is the GVR of the request.
// It makes it easy to create errors such as in:
// apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
GroupVersionResource schema.GroupVersionResource
}
// Store should provide all required operations to serve a given resource. A
// resource is defined by the resource itself (T) and a list type for the resource (TList).
// For example, Store[*Token, *TokenList] is a store that allows CRUD operations on *Token
// objects and allows listing tokens in a *TokenList object.
//
// Store does not define the backing storage for a resource. The storage is
// up to the implementer. For example, resources could be stored in another ETCD
// database, in a SQLite database, in another built-in resource such as Secrets.
// It is also possible to have no storage at all.
//
// Errors returned by the Store should use errors from k8s.io/apimachinery/pkg/api/errors. This
// will ensure that the right error will be returned to the clients (eg: kubectl, client-go) so
// they can react accordingly. For example, if an object is not found, store should
// return the following error:
//
// apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
//
// Stores should make use of the various metav1.*Options as best as possible.
// Those options are the same options coming from client-go or kubectl, generally
// meant to control the behavior of the stores. Note: We currently don't have
// field-manager enabled.
type Store[T runtime.Object, TList runtime.Object] interface {
// Create should store the resource to some backing storage.
//
// It can apply modifications as necessary before storing it. It must
// return a resource of the type of the store, but can
// create/update/delete arbitrary objects in Kubernetes without
// returning them to the user.
//
// It is called either when a request creates a resource, or when a
// request updates a resource that doesn't exist.
Create(ctx Context, obj T, opts *metav1.CreateOptions) (T, error)
// Update should overwrite a resource that is present in the backing storage.
//
// It can apply modifications as necessary before storing it. It must
// return a resource of the type of the store, but can
// create/update/delete arbitrary objects in Kubernetes without
// returning them to the user.
//
// It is called when a request updates a resource (eg: through a patch or update request)
Update(ctx Context, obj T, opts *metav1.UpdateOptions) (T, error)
// Get retrieves the resource with the given name from the backing storage.
//
// Get is called for the following requests:
// - get requests: The object must be returned.
// - update requests: The object is needed to apply a JSON patch and to make some validation on the change.
// - delete requests: The object is needed to make some validation on it.
Get(ctx Context, name string, opts *metav1.GetOptions) (T, error)
// List retrieves all resources matching the given ListOptions from the backing storage.
List(ctx Context, opts *metav1.ListOptions) (TList, error)
// Watch sends change events to a returned channel.
//
// The store is responsible for closing the channel.
Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[T], error)
// Delete deletes the resource of the given name from the backing storage.
Delete(ctx Context, name string, opts *metav1.DeleteOptions) error
}
type WatchEvent[T runtime.Object] struct {
Event watch.EventType
Object T
}

123
pkg/ext/testdata/rbac.yaml vendored Normal file
View File

@ -0,0 +1,123 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: read-only
rules:
- apiGroups: ["ext.cattle.io"]
verbs: ["list", "get", "watch"]
resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: read-write
rules:
- apiGroups: ["ext.cattle.io"]
verbs: ["list", "get", "watch", "create", "update", "patch", "delete"]
resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: update-not-create
rules:
- apiGroups: ["ext.cattle.io"]
verbs: ["list", "get", "watch", "update"]
resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: all
rules:
- apiGroups: ["ext.cattle.io"]
verbs: ["*"]
resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: other
rules:
- apiGroups: ["management.cattle.io"]
verbs: ["*"]
resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-only
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: read-only
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: read-only
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-write
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: read-write
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: read-write
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: update-not-create
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: update-not-create
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: update-not-create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: all
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: all
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: all
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: other
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: other
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: other
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: read-only-error
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: read-only
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: read-only-error

View File

@ -17,7 +17,7 @@ import (
) )
func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler, func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler,
routerFunc router.RouterFunc) (*apiserver.Server, http.Handler, error) { routerFunc router.RouterFunc, extensionAPIServer http.Handler) (*apiserver.Server, http.Handler, error) {
var ( var (
proxy http.Handler proxy http.Handler
err error err error
@ -46,6 +46,9 @@ func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, ne
K8sProxy: w(proxy), K8sProxy: w(proxy),
APIRoot: w(a.apiHandler(apiRoot)), APIRoot: w(a.apiHandler(apiRoot)),
} }
if extensionAPIServer != nil {
handlers.ExtensionAPIServer = w(extensionAPIServer)
}
if routerFunc == nil { if routerFunc == nil {
return a.server, router.Routes(handlers), nil return a.server, router.Routes(handlers), nil
} }

View File

@ -14,6 +14,9 @@ type Handlers struct {
APIRoot http.Handler APIRoot http.Handler
K8sProxy http.Handler K8sProxy http.Handler
Next http.Handler Next http.Handler
// ExtensionAPIServer serves under /ext. If nil, the default unknown path
// handler is served.
ExtensionAPIServer http.Handler
} }
func Routes(h Handlers) http.Handler { func Routes(h Handlers) http.Handler {
@ -25,6 +28,11 @@ func Routes(h Handlers) http.Handler {
m.Path("/").Handler(h.APIRoot).HeadersRegexp("Accept", ".*json.*") m.Path("/").Handler(h.APIRoot).HeadersRegexp("Accept", ".*json.*")
m.Path("/{name:v1}").Handler(h.APIRoot) m.Path("/{name:v1}").Handler(h.APIRoot)
if h.ExtensionAPIServer != nil {
m.Path("/ext").Handler(http.StripPrefix("/ext", h.ExtensionAPIServer))
m.PathPrefix("/ext/").Handler(http.StripPrefix("/ext", h.ExtensionAPIServer))
}
m.Path("/v1/{type}").Handler(h.K8sResource) m.Path("/v1/{type}").Handler(h.K8sResource)
m.Path("/v1/{type}/{nameorns}").Queries("link", "{link}").Handler(h.K8sResource) m.Path("/v1/{type}/{nameorns}").Queries("link", "{link}").Handler(h.K8sResource)
m.Path("/v1/{type}/{nameorns}").Queries("action", "{action}").Handler(h.K8sResource) m.Path("/v1/{type}/{nameorns}").Queries("action", "{action}").Handler(h.K8sResource)

View File

@ -31,6 +31,16 @@ import (
var ErrConfigRequired = errors.New("rest config is required") var ErrConfigRequired = errors.New("rest config is required")
// ExtensionAPIServer will run an extension API server. The extension API server
// will be accessible from Steve at the /ext endpoint and will be compatible with
// the aggregate API server in Kubernetes.
type ExtensionAPIServer interface {
// The ExtensionAPIServer is served at /ext in Steve's mux
http.Handler
// Run configures the API server and make the HTTP handler available
Run(ctx context.Context)
}
type Server struct { type Server struct {
http.Handler http.Handler
@ -44,6 +54,8 @@ type Server struct {
ClusterRegistry string ClusterRegistry string
Version string Version string
extensionAPIServer ExtensionAPIServer
authMiddleware auth.Middleware authMiddleware auth.Middleware
controllers *Controllers controllers *Controllers
needControllerStart bool needControllerStart bool
@ -69,6 +81,14 @@ type Options struct {
ServerVersion string ServerVersion string
// SQLCache enables the SQLite-based lasso caching mechanism // SQLCache enables the SQLite-based lasso caching mechanism
SQLCache bool SQLCache bool
// ExtensionAPIServer enables an extension API server that will be served
// under /ext
// If nil, Steve's default http handler for unknown routes will be served.
//
// In most cases, you'll want to use [github.com/rancher/steve/pkg/ext.NewExtensionAPIServer]
// to create an ExtensionAPIServer.
ExtensionAPIServer ExtensionAPIServer
} }
func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server, error) { func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server, error) {
@ -89,7 +109,8 @@ func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server,
ClusterRegistry: opts.ClusterRegistry, ClusterRegistry: opts.ClusterRegistry,
Version: opts.ServerVersion, Version: opts.ServerVersion,
// SQLCache enables the SQLite-based lasso caching mechanism // SQLCache enables the SQLite-based lasso caching mechanism
SQLCache: opts.SQLCache, SQLCache: opts.SQLCache,
extensionAPIServer: opts.ExtensionAPIServer,
} }
if err := setup(ctx, server); err != nil { if err := setup(ctx, server); err != nil {
@ -213,7 +234,7 @@ func setup(ctx context.Context, server *Server) error {
onSchemasHandler, onSchemasHandler,
sf) sf)
apiServer, handler, err := handler.New(server.RESTConfig, sf, server.authMiddleware, server.next, server.router) apiServer, handler, err := handler.New(server.RESTConfig, sf, server.authMiddleware, server.next, server.router, server.extensionAPIServer)
if err != nil { if err != nil {
return err return err
} }
@ -231,6 +252,9 @@ func (c *Server) start(ctx context.Context) error {
return err return err
} }
} }
if c.extensionAPIServer != nil {
c.extensionAPIServer.Run(ctx)
}
return nil return nil
} }

View File

@ -1,3 +1,13 @@
#!/bin/bash #!/bin/bash
go test ./... if ! command -v setup-envtest; then
echo "setup-envtest is required for tests, but was not installed"
echo "see the 'Running Tests' section of the readme for install instructions"
exit 127
fi
minor=$(go list -m all | grep 'k8s.io/client-go' | cut -d ' ' -f 2 | cut -d '.' -f 2)
version="1.$minor.x"
export KUBEBUILDER_ASSETS=$(setup-envtest use -p path "$version")
go test ./...