mirror of
https://github.com/rancher/steve.git
synced 2025-04-27 19:05:09 +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:
parent
57a25ffa82
commit
1f21e5e515
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -25,6 +25,8 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
|
||||
with:
|
||||
version: v1.59.0
|
||||
- name: Install env-test
|
||||
run: go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||
- name: Build
|
||||
run: make build-bin
|
||||
- name: Test
|
||||
|
18
README.md
18
README.md
@ -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).
|
||||
See the documentation included there for running the tests and using them to
|
||||
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
23
go.mod
@ -40,19 +40,27 @@ require (
|
||||
k8s.io/klog v1.0.0
|
||||
k8s.io/kube-aggregator v0.31.1
|
||||
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
|
||||
sigs.k8s.io/controller-runtime v0.19.0
|
||||
)
|
||||
|
||||
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/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // 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 v5.9.0 // 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/ghodss/yaml v1.0.0 // 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/swag v0.22.4 // 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/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.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/hashicorp/golang-lru/v2 v2.0.7 // 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/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
@ -83,9 +95,15 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // 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/otel 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/trace v1.28.0 // 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/exp v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
@ -107,9 +128,11 @@ require (
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // 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
|
||||
k8s.io/component-base v0.31.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
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.49.3 // indirect
|
||||
|
80
go.sum
80
go.sum
@ -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 v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
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/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/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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
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 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 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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.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/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-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/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.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
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/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.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
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/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.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/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
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/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
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/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
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.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
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/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 v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@ -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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
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/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
|
||||
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
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/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/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.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/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
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/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
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.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-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-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/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
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/yaml.v2 v2.2.1/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/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
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/go.mod h1:+aW4NX50uneozN+BtoCxI4g7ND922p8Wy3tWKFDiWVk=
|
||||
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/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg=
|
||||
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/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
|
277
pkg/ext/apiserver.go
Normal file
277
pkg/ext/apiserver.go
Normal 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
|
||||
}
|
||||
}
|
170
pkg/ext/apiserver_authentication_test.go
Normal file
170
pkg/ext/apiserver_authentication_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
47
pkg/ext/apiserver_authorization.go
Normal file
47
pkg/ext/apiserver_authorization.go
Normal 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
|
||||
}
|
344
pkg/ext/apiserver_authorization_test.go
Normal file
344
pkg/ext/apiserver_authorization_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
50
pkg/ext/apiserver_suite_test.go
Normal file
50
pkg/ext/apiserver_suite_test.go
Normal 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
770
pkg/ext/apiserver_test.go
Normal 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
351
pkg/ext/delegate.go
Normal 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
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
93
pkg/ext/store.go
Normal 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
123
pkg/ext/testdata/rbac.yaml
vendored
Normal 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
|
@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
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 (
|
||||
proxy http.Handler
|
||||
err error
|
||||
@ -46,6 +46,9 @@ func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, ne
|
||||
K8sProxy: w(proxy),
|
||||
APIRoot: w(a.apiHandler(apiRoot)),
|
||||
}
|
||||
if extensionAPIServer != nil {
|
||||
handlers.ExtensionAPIServer = w(extensionAPIServer)
|
||||
}
|
||||
if routerFunc == nil {
|
||||
return a.server, router.Routes(handlers), nil
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ type Handlers struct {
|
||||
APIRoot http.Handler
|
||||
K8sProxy 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 {
|
||||
@ -25,6 +28,11 @@ func Routes(h Handlers) http.Handler {
|
||||
m.Path("/").Handler(h.APIRoot).HeadersRegexp("Accept", ".*json.*")
|
||||
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}/{nameorns}").Queries("link", "{link}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{type}/{nameorns}").Queries("action", "{action}").Handler(h.K8sResource)
|
||||
|
@ -31,6 +31,16 @@ import (
|
||||
|
||||
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 {
|
||||
http.Handler
|
||||
|
||||
@ -44,6 +54,8 @@ type Server struct {
|
||||
ClusterRegistry string
|
||||
Version string
|
||||
|
||||
extensionAPIServer ExtensionAPIServer
|
||||
|
||||
authMiddleware auth.Middleware
|
||||
controllers *Controllers
|
||||
needControllerStart bool
|
||||
@ -69,6 +81,14 @@ type Options struct {
|
||||
ServerVersion string
|
||||
// SQLCache enables the SQLite-based lasso caching mechanism
|
||||
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) {
|
||||
@ -89,7 +109,8 @@ func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server,
|
||||
ClusterRegistry: opts.ClusterRegistry,
|
||||
Version: opts.ServerVersion,
|
||||
// SQLCache enables the SQLite-based lasso caching mechanism
|
||||
SQLCache: opts.SQLCache,
|
||||
SQLCache: opts.SQLCache,
|
||||
extensionAPIServer: opts.ExtensionAPIServer,
|
||||
}
|
||||
|
||||
if err := setup(ctx, server); err != nil {
|
||||
@ -213,7 +234,7 @@ func setup(ctx context.Context, server *Server) error {
|
||||
onSchemasHandler,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -231,6 +252,9 @@ func (c *Server) start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.extensionAPIServer != nil {
|
||||
c.extensionAPIServer.Run(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,13 @@
|
||||
#!/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 ./...
|
||||
|
Loading…
Reference in New Issue
Block a user