mirror of
https://github.com/rancher/steve.git
synced 2025-08-10 02:37:17 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
458a12d2d7 | ||
|
6b5543e762 | ||
|
b88abd9aa0 | ||
|
a2116cfa52 | ||
|
c4ebbe629f | ||
|
ddd2e373b7 | ||
|
6e30359c65 | ||
|
7dafe0c662 | ||
|
dd27bd0c8d | ||
|
aacb5b82de | ||
|
79304d93b4 | ||
|
096afba4f7 | ||
|
61be17faa3 | ||
|
1464a203c3 | ||
|
643002b00f | ||
|
244a1993e0 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Install golangci-lint
|
- name: Install golangci-lint
|
||||||
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
|
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
|
||||||
with:
|
with:
|
||||||
version: v1.59.0
|
version: v1.63.4
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-bin
|
run: make build-bin
|
||||||
- name: Test
|
- name: Test
|
||||||
|
21
.github/workflows/release.yaml
vendored
Normal file
21
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name : Checkout repository
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
|
- name: Create release on Github
|
||||||
|
run: |
|
||||||
|
gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@ -1,5 +1,5 @@
|
|||||||
# syntax = docker/dockerfile:experimental
|
# syntax = docker/dockerfile:experimental
|
||||||
FROM registry.suse.com/bci/golang:1.22 as build
|
FROM registry.suse.com/bci/golang:1.23 as build
|
||||||
COPY go.mod go.sum main.go /src/
|
COPY go.mod go.sum main.go /src/
|
||||||
COPY pkg /src/pkg/
|
COPY pkg /src/pkg/
|
||||||
#RUN --mount=type=cache,target=/root/.cache/go-build \
|
#RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
11
README.md
11
README.md
@ -124,6 +124,17 @@ item is included in the list.
|
|||||||
/v1/{type}?filter=spec.containers.image=alpine
|
/v1/{type}?filter=spec.containers.image=alpine
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**If SQLite caching is enabled** (`server.Options.SQLCache=true`),
|
||||||
|
filtering is only supported for a subset of attributes:
|
||||||
|
- `id`, `metadata.name`, `metadata.namespace`, `metadata.state.name`, and `metadata.timestamp` for any resource kind
|
||||||
|
- a short list of hardcoded attributes for a selection of specific types listed
|
||||||
|
in [typeSpecificIndexFields](https://github.com/rancher/steve/blob/main/pkg/stores/sqlproxy/proxy_store.go#L52-L58)
|
||||||
|
- the special string `metadata.fields[N]`, with N starting at 0, for all columns
|
||||||
|
displayed by `kubectl get $TYPE`. For example `secrets` have `"metadata.fields[0]"`,
|
||||||
|
`"metadata.fields[1]"` , `"metadata.fields[2]"`, and `"metadata.fields[3]"` respectively
|
||||||
|
corresponding to `"name"`, `"type"`, `"data"`, and `"age"`. For CRDs, these come from
|
||||||
|
[Additional printer columns](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns)
|
||||||
|
|
||||||
#### `projectsornamespaces`
|
#### `projectsornamespaces`
|
||||||
|
|
||||||
Resources can also be filtered by the Rancher projects their namespaces belong
|
Resources can also be filtered by the Rancher projects their namespaces belong
|
||||||
|
105
go.mod
105
go.mod
@ -1,8 +1,47 @@
|
|||||||
module github.com/rancher/steve
|
module github.com/rancher/steve
|
||||||
|
|
||||||
go 1.22.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.22.2
|
toolchain go1.23.4
|
||||||
|
|
||||||
|
// pin Kubernetes dependencies to v0.30.1, which is the version
|
||||||
|
// Rancher 2.9 is compiled against as newer lasso versions
|
||||||
|
// pull in newer Kubernetes dependencies
|
||||||
|
replace (
|
||||||
|
github.com/google/cel-go => github.com/google/cel-go v0.17.8
|
||||||
|
github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.16.0
|
||||||
|
github.com/prometheus/client_model => github.com/prometheus/client_model v0.4.0
|
||||||
|
github.com/prometheus/common => github.com/prometheus/common v0.44.0
|
||||||
|
k8s.io/api => k8s.io/api v0.30.1
|
||||||
|
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.30.1
|
||||||
|
k8s.io/apimachinery => k8s.io/apimachinery v0.30.1
|
||||||
|
k8s.io/apiserver => k8s.io/apiserver v0.30.1
|
||||||
|
k8s.io/cli-runtime => k8s.io/cli-runtime v0.30.1
|
||||||
|
k8s.io/client-go => k8s.io/client-go v0.30.1
|
||||||
|
k8s.io/cloud-provider => k8s.io/cloud-provider v0.30.1
|
||||||
|
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.30.1
|
||||||
|
k8s.io/code-generator => k8s.io/code-generator v0.30.1
|
||||||
|
k8s.io/component-base => k8s.io/component-base v0.30.1
|
||||||
|
k8s.io/component-helpers => k8s.io/component-helpers v0.30.1
|
||||||
|
k8s.io/controller-manager => k8s.io/controller-manager v0.30.1
|
||||||
|
k8s.io/cri-api => k8s.io/cri-api v0.30.1
|
||||||
|
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.30.1
|
||||||
|
k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.30.1
|
||||||
|
k8s.io/endpointslice => k8s.io/endpointslice v0.30.1
|
||||||
|
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.30.1
|
||||||
|
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.30.1
|
||||||
|
k8s.io/kube-proxy => k8s.io/kube-proxy v0.30.1
|
||||||
|
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.30.1
|
||||||
|
k8s.io/kubectl => k8s.io/kubectl v0.30.1
|
||||||
|
k8s.io/kubelet => k8s.io/kubelet v0.30.1
|
||||||
|
k8s.io/kubernetes => k8s.io/kubernetes v1.30.1
|
||||||
|
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.30.1
|
||||||
|
k8s.io/metrics => k8s.io/metrics v0.30.1
|
||||||
|
k8s.io/mount-utils => k8s.io/mount-utils v0.30.1
|
||||||
|
k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.30.1
|
||||||
|
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.30.1
|
||||||
|
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.18.5
|
||||||
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/crewjam/saml => github.com/rancher/saml v0.2.0
|
github.com/crewjam/saml => github.com/rancher/saml v0.2.0
|
||||||
@ -13,60 +52,63 @@ replace (
|
|||||||
require (
|
require (
|
||||||
github.com/adrg/xdg v0.4.0
|
github.com/adrg/xdg v0.4.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
|
github.com/golang/protobuf v1.5.4
|
||||||
github.com/google/gnostic-models v0.6.8
|
github.com/google/gnostic-models v0.6.8
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/pborman/uuid v1.2.1
|
github.com/pborman/uuid v1.2.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.16.0
|
github.com/prometheus/client_golang v1.19.1
|
||||||
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
|
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
|
||||||
github.com/rancher/dynamiclistener v0.6.0-rc2
|
github.com/rancher/dynamiclistener v0.6.0-rc2
|
||||||
github.com/rancher/kubernetes-provider-detector v0.1.5
|
github.com/rancher/kubernetes-provider-detector v0.1.5
|
||||||
github.com/rancher/lasso v0.0.0-20240805175815-a40054127062
|
github.com/rancher/lasso v0.0.0-20241202185148-04649f379358
|
||||||
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9
|
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9
|
||||||
github.com/rancher/remotedialer v0.3.2
|
github.com/rancher/remotedialer v0.3.2
|
||||||
github.com/rancher/wrangler/v3 v3.0.0
|
github.com/rancher/wrangler/v3 v3.0.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/urfave/cli v1.22.14
|
github.com/urfave/cli v1.22.14
|
||||||
github.com/urfave/cli/v2 v2.27.3
|
github.com/urfave/cli/v2 v2.27.1
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.7.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.30.1
|
helm.sh/helm/v3 v3.13.0
|
||||||
k8s.io/apiextensions-apiserver v0.30.1
|
k8s.io/api v0.31.1
|
||||||
k8s.io/apimachinery v0.30.1
|
k8s.io/apiextensions-apiserver v0.31.0
|
||||||
|
k8s.io/apimachinery v0.31.1
|
||||||
k8s.io/apiserver v0.30.1
|
k8s.io/apiserver v0.30.1
|
||||||
k8s.io/client-go v0.30.1
|
k8s.io/client-go v0.31.1
|
||||||
|
k8s.io/helm v2.17.0+incompatible
|
||||||
k8s.io/klog v1.0.0
|
k8s.io/klog v1.0.0
|
||||||
k8s.io/kube-aggregator v0.30.1
|
k8s.io/kube-aggregator v0.30.1
|
||||||
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
|
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
github.com/go-openapi/swag v0.22.3 // indirect
|
github.com/go-openapi/swag v0.22.4 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
@ -77,14 +119,14 @@ require (
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.4.0 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.19.0 // indirect
|
go.opentelemetry.io/otel v1.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
|
||||||
@ -93,23 +135,22 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||||
golang.org/x/crypto v0.22.0 // indirect
|
golang.org/x/crypto v0.24.0 // indirect
|
||||||
golang.org/x/net v0.24.0 // indirect
|
golang.org/x/net v0.26.0 // indirect
|
||||||
golang.org/x/oauth2 v0.16.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.21.0 // indirect
|
||||||
golang.org/x/term v0.19.0 // indirect
|
golang.org/x/term v0.21.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||||
google.golang.org/grpc v1.58.3 // indirect
|
google.golang.org/grpc v1.58.3 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
k8s.io/component-base v0.30.1 // indirect
|
k8s.io/component-base v0.30.1 // indirect
|
||||||
k8s.io/klog/v2 v2.120.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
modernc.org/libc v1.49.3 // indirect
|
modernc.org/libc v1.49.3 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
@ -121,5 +162,5 @@ require (
|
|||||||
sigs.k8s.io/cli-utils v0.35.0 // indirect
|
sigs.k8s.io/cli-utils v0.35.0 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
2
main.go
2
main.go
@ -34,7 +34,7 @@ func main() {
|
|||||||
func run(_ *cli.Context) error {
|
func run(_ *cli.Context) error {
|
||||||
ctx := signals.SetupSignalContext()
|
ctx := signals.SetupSignalContext()
|
||||||
debugconfig.MustSetupDebug()
|
debugconfig.MustSetupDebug()
|
||||||
s, err := config.ToServer(ctx, false)
|
s, err := config.ToServer(ctx, debugconfig.SQLCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ func isListOrGetable(schema *types.APISchema) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isListWatchable(schema *types.APISchema) bool {
|
func IsListWatchable(schema *types.APISchema) bool {
|
||||||
var (
|
var (
|
||||||
canList bool
|
canList bool
|
||||||
canWatch bool
|
canWatch bool
|
||||||
@ -163,7 +163,7 @@ func (h *handler) refreshAll(ctx context.Context) error {
|
|||||||
|
|
||||||
filteredSchemas := map[string]*types.APISchema{}
|
filteredSchemas := map[string]*types.APISchema{}
|
||||||
for _, schema := range schemas {
|
for _, schema := range schemas {
|
||||||
if isListWatchable(schema) {
|
if IsListWatchable(schema) {
|
||||||
if preferredTypeExists(schema, schemas) {
|
if preferredTypeExists(schema, schemas) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Debug bool
|
Debug bool
|
||||||
DebugLevel int
|
DebugLevel int
|
||||||
|
SQLCache bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) MustSetupDebug() {
|
func (c *Config) MustSetupDebug() {
|
||||||
@ -54,6 +55,10 @@ func Flags(config *Config) []cli.Flag {
|
|||||||
Value: 7,
|
Value: 7,
|
||||||
Destination: &config.DebugLevel,
|
Destination: &config.DebugLevel,
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "sql-cache",
|
||||||
|
Destination: &config.SQLCache,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,5 +73,9 @@ func FlagsV2(config *Config) []cliv2.Flag {
|
|||||||
Value: 7,
|
Value: 7,
|
||||||
Destination: &config.DebugLevel,
|
Destination: &config.DebugLevel,
|
||||||
},
|
},
|
||||||
|
&cliv2.BoolFlag{
|
||||||
|
Name: "sql-cache",
|
||||||
|
Destination: &config.SQLCache,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,8 @@ func formatter(summarycache *summarycache.SummaryCache) types.Formatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
||||||
|
// with the sql cache, these were already added by the indexer. However, the sql cache
|
||||||
|
// is only used for lists, so we need to re-add here for get/watch
|
||||||
s, rel := summarycache.SummaryAndRelationship(unstr)
|
s, rel := summarycache.SummaryAndRelationship(unstr)
|
||||||
data.PutValue(unstr.Object, map[string]interface{}{
|
data.PutValue(unstr.Object, map[string]interface{}{
|
||||||
"name": s.State,
|
"name": s.State,
|
||||||
|
@ -1,17 +1,59 @@
|
|||||||
package formatters
|
package formatters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
"github.com/rancher/norman/types/convert"
|
"github.com/rancher/norman/types/convert"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/data"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"helm.sh/helm/v3/pkg/release"
|
||||||
|
rspb "k8s.io/helm/pkg/proto/hapi/release"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DropHelmData(request *types.APIRequest, resource *types.RawResource) {
|
var (
|
||||||
data := resource.APIObject.Data()
|
ErrNotHelmRelease = errors.New("not helm release") // error for when it's not a helm release
|
||||||
if data.String("metadata", "labels", "owner") == "helm" ||
|
magicGzip = []byte{0x1f, 0x8b, 0x08} // gzip magic header
|
||||||
data.String("metadata", "labels", "OWNER") == "TILLER" {
|
)
|
||||||
if data.String("data", "release") != "" {
|
|
||||||
delete(data.Map("data"), "release")
|
func HandleHelmData(request *types.APIRequest, resource *types.RawResource) {
|
||||||
|
objData := resource.APIObject.Data()
|
||||||
|
if q := request.Query.Get("includeHelmData"); q == "true" {
|
||||||
|
var helmReleaseData string
|
||||||
|
if resource.Type == "secret" {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(objData.String("data", "release"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helmReleaseData = string(b)
|
||||||
|
} else {
|
||||||
|
helmReleaseData = objData.String("data", "release")
|
||||||
}
|
}
|
||||||
|
if objData.String("metadata", "labels", "owner") == "helm" {
|
||||||
|
rl, err := decodeHelm3(helmReleaseData)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to decode helm3 release data: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objData.SetNested(rl, "data", "release")
|
||||||
|
}
|
||||||
|
if objData.String("metadata", "labels", "OWNER") == "TILLER" {
|
||||||
|
rl, err := decodeHelm2(helmReleaseData)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to decode helm2 release data: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objData.SetNested(rl, "data", "release")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
DropHelmData(objData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,3 +64,78 @@ func Pod(request *types.APIRequest, resource *types.RawResource) {
|
|||||||
data.SetNested(convert.LowerTitle(fields[2]), "metadata", "state", "name")
|
data.SetNested(convert.LowerTitle(fields[2]), "metadata", "state", "name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeHelm3 receives a helm3 release data string, decodes the string data using the standard base64 library
|
||||||
|
// and unmarshals the data into release.Release struct to return it.
|
||||||
|
func decodeHelm3(data string) (*release.Release, error) {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data is too small to be helm 3 release object
|
||||||
|
if len(b) <= 3 {
|
||||||
|
return nil, ErrNotHelmRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compatibility with releases that were stored before
|
||||||
|
// compression was introduced we skip decompression if the
|
||||||
|
// gzip magic header is not found
|
||||||
|
if bytes.Equal(b[0:3], magicGzip) {
|
||||||
|
r, err := gzip.NewReader(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b2, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b = b2
|
||||||
|
}
|
||||||
|
|
||||||
|
var rls release.Release
|
||||||
|
// unmarshal release object bytes
|
||||||
|
if err := json.Unmarshal(b, &rls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeHelm2 receives a helm2 release data and returns the corresponding helm2 release proto struct
|
||||||
|
func decodeHelm2(data string) (*rspb.Release, error) {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compatibility with releases that were stored before
|
||||||
|
// compression was introduced we skip decompression if the
|
||||||
|
// gzip magic header is not found
|
||||||
|
if bytes.Equal(b[0:3], magicGzip) {
|
||||||
|
r, err := gzip.NewReader(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b2, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b = b2
|
||||||
|
}
|
||||||
|
|
||||||
|
var rls rspb.Release
|
||||||
|
// unmarshal protobuf bytes
|
||||||
|
if err := proto.Unmarshal(b, &rls); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DropHelmData(data data.Object) {
|
||||||
|
if data.String("metadata", "labels", "owner") == "helm" ||
|
||||||
|
data.String("metadata", "labels", "OWNER") == "TILLER" {
|
||||||
|
if data.String("data", "release") != "" {
|
||||||
|
delete(data.Map("data"), "release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
380
pkg/resources/formatters/formatter_test.go
Normal file
380
pkg/resources/formatters/formatter_test.go
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"helm.sh/helm/v3/pkg/chart"
|
||||||
|
"helm.sh/helm/v3/pkg/release"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
pbchart "k8s.io/helm/pkg/proto/hapi/chart"
|
||||||
|
rspb "k8s.io/helm/pkg/proto/hapi/release"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var r = release.Release{
|
||||||
|
Name: "helmV3Release",
|
||||||
|
Chart: &chart.Chart{
|
||||||
|
Values: map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Namespace: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
var rv2 = rspb.Release{
|
||||||
|
Name: "helmV3Release",
|
||||||
|
Chart: &pbchart.Chart{
|
||||||
|
Metadata: &pbchart.Metadata{
|
||||||
|
Name: "chartName",
|
||||||
|
Version: "1.0.0",
|
||||||
|
},
|
||||||
|
Values: &pbchart.Config{
|
||||||
|
Values: map[string]*pbchart.Value{
|
||||||
|
"key": {Value: "value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Version: 1,
|
||||||
|
Namespace: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_HandleHelmData(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resource *types.RawResource
|
||||||
|
request *types.APIRequest
|
||||||
|
want *types.RawResource
|
||||||
|
helmVersion int
|
||||||
|
}{ //helm v3
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource with includeHelmData set to TRUE and owner set to HELM, it should decode the helm3 release",
|
||||||
|
resource: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV3Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": &r,
|
||||||
|
}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource with includeHelmData set to FALSE and owner set to HELM, it should drop the helm data",
|
||||||
|
resource: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV3Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest("false"),
|
||||||
|
want: newSecret("helm", map[string]interface{}{}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource WITHOUT the includeHelmData query parameter and owner set to HELM, it should drop the helm data",
|
||||||
|
resource: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV3Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest(""),
|
||||||
|
want: newSecret("helm", map[string]interface{}{}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a non-helm SECRET or CONFIGMAP resource with includeHelmData set to TRUE, it shouldn't change the resource",
|
||||||
|
resource: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a non-helm SECRET or CONFIGMAP resource with includeHelmData set to FALSE, it shouldn't change the resource",
|
||||||
|
resource: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
request: newRequest("false"),
|
||||||
|
want: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a non-helm SECRET or CONFIGMAP resource WITHOUT the includeHelmData query parameter, it shouldn't change the resource",
|
||||||
|
resource: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
request: newRequest(""),
|
||||||
|
want: &types.RawResource{
|
||||||
|
Type: "pod",
|
||||||
|
APIObject: types.APIObject{Object: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource with includeHelmData set to TRUE and owner set to HELM, it should decode the helm3 release",
|
||||||
|
resource: newConfigMap("helm", map[string]interface{}{
|
||||||
|
"release": newV3Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newConfigMap("helm", map[string]interface{}{
|
||||||
|
"release": &r,
|
||||||
|
}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource with includeHelmData set to FALSE and owner set to HELM, it should drop the helm data",
|
||||||
|
resource: newConfigMap("helm", map[string]interface{}{
|
||||||
|
"release": newV3Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest("false"),
|
||||||
|
want: newConfigMap("helm", map[string]interface{}{}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource WITHOUT the includeHelmData query parameter and owner set to HELM, it should drop the helm data",
|
||||||
|
resource: newConfigMap("helm", map[string]interface{}{
|
||||||
|
"release": newV3Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest(""),
|
||||||
|
want: newConfigMap("helm", map[string]interface{}{}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
//helm v2
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource with includeHelmData set to TRUE and owner set to TILLER, it should decode the helm2 release",
|
||||||
|
resource: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV2Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": &rv2,
|
||||||
|
}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource with includeHelmData set to FALSE and owner set to TILLER, it should drop the helm data",
|
||||||
|
resource: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV2Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest("false"),
|
||||||
|
want: newSecret("TILLER", map[string]interface{}{}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a SECRET resource WITHOUT the includeHelmData query parameter and owner set to TILLER, it should drop the helm data",
|
||||||
|
resource: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV2Release())),
|
||||||
|
}),
|
||||||
|
request: newRequest(""),
|
||||||
|
want: newSecret("TILLER", map[string]interface{}{}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource with includeHelmData set to TRUE and owner set to TILLER, it should decode the helm2 release",
|
||||||
|
resource: newConfigMap("TILLER", map[string]interface{}{
|
||||||
|
"release": newV2Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newConfigMap("TILLER", map[string]interface{}{
|
||||||
|
"release": &rv2,
|
||||||
|
}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource with includeHelmData set to FALSE and owner set to TILLER, it should drop the helm data",
|
||||||
|
resource: newConfigMap("TILLER", map[string]interface{}{
|
||||||
|
"release": newV2Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest("false"),
|
||||||
|
want: newConfigMap("TILLER", map[string]interface{}{}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When receiving a CONFIGMAP resource WITHOUT the includeHelmData query parameter and owner set to TILLER, it should drop the helm data",
|
||||||
|
resource: newConfigMap("TILLER", map[string]interface{}{
|
||||||
|
"release": newV2Release(),
|
||||||
|
}),
|
||||||
|
request: newRequest(""),
|
||||||
|
want: newConfigMap("TILLER", map[string]interface{}{}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[no magic gzip] When receiving a SECRET resource with includeHelmData set to TRUE and owner set to HELM, it should decode the helm3 release",
|
||||||
|
resource: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV3ReleaseWithoutGzip())),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newSecret("helm", map[string]interface{}{
|
||||||
|
"release": &r,
|
||||||
|
}),
|
||||||
|
helmVersion: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[no magic gzip] When receiving a SECRET resource with includeHelmData set to TRUE and owner set to TILLER, it should decode the helm2 release",
|
||||||
|
resource: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": base64.StdEncoding.EncodeToString([]byte(newV2ReleaseWithoutGzip())),
|
||||||
|
}),
|
||||||
|
request: newRequest("true"),
|
||||||
|
want: newSecret("TILLER", map[string]interface{}{
|
||||||
|
"release": &rv2,
|
||||||
|
}),
|
||||||
|
helmVersion: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
HandleHelmData(tt.request, tt.resource)
|
||||||
|
if tt.helmVersion == 2 {
|
||||||
|
u, ok := tt.resource.APIObject.Object.(*unstructured.Unstructured)
|
||||||
|
assert.True(t, ok)
|
||||||
|
rl, ok := u.UnstructuredContent()["data"].(map[string]interface{})["release"]
|
||||||
|
if ok {
|
||||||
|
u, ok = tt.want.APIObject.Object.(*unstructured.Unstructured)
|
||||||
|
assert.True(t, ok)
|
||||||
|
rl2, ok := u.UnstructuredContent()["data"].(map[string]interface{})["release"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.True(t, proto.Equal(rl.(proto.Message), rl2.(proto.Message)))
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.resource, tt.want)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.resource, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecret(owner string, data map[string]interface{}) *types.RawResource {
|
||||||
|
secret := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"data": data,
|
||||||
|
}}
|
||||||
|
if owner == "helm" {
|
||||||
|
secret.SetLabels(map[string]string{"owner": owner})
|
||||||
|
}
|
||||||
|
if owner == "TILLER" {
|
||||||
|
secret.SetLabels(map[string]string{"OWNER": owner})
|
||||||
|
}
|
||||||
|
return &types.RawResource{
|
||||||
|
Type: "secret",
|
||||||
|
APIObject: types.APIObject{Object: secret},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfigMap(owner string, data map[string]interface{}) *types.RawResource {
|
||||||
|
cfgMap := &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "configmap",
|
||||||
|
"data": data,
|
||||||
|
}}
|
||||||
|
if owner == "helm" {
|
||||||
|
cfgMap.SetLabels(map[string]string{"owner": owner})
|
||||||
|
}
|
||||||
|
if owner == "TILLER" {
|
||||||
|
cfgMap.SetLabels(map[string]string{"OWNER": owner})
|
||||||
|
}
|
||||||
|
return &types.RawResource{
|
||||||
|
Type: "configmap",
|
||||||
|
APIObject: types.APIObject{Object: cfgMap},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newV2Release() string {
|
||||||
|
a := rv2
|
||||||
|
b, err := proto.Marshal(&a)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to marshal release: %v", err)
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
gz := gzip.NewWriter(&buf)
|
||||||
|
gz.Write(b)
|
||||||
|
gz.Close()
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newV2ReleaseWithoutGzip() string {
|
||||||
|
a := rv2
|
||||||
|
b, err := proto.Marshal(&a)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to marshal release: %v", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newV3Release() string {
|
||||||
|
b, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to marshal release: %v", err)
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
gz := gzip.NewWriter(&buf)
|
||||||
|
gz.Write(b)
|
||||||
|
gz.Close()
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newV3ReleaseWithoutGzip() string {
|
||||||
|
b, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to marshal release: %v", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(value string) *types.APIRequest {
|
||||||
|
req := &types.APIRequest{Query: url.Values{}}
|
||||||
|
if value != "" {
|
||||||
|
req.Query.Add("includeHelmData", value)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
@ -53,11 +53,11 @@ func DefaultSchemaTemplates(cf *client.Factory,
|
|||||||
apigroups.Template(discovery),
|
apigroups.Template(discovery),
|
||||||
{
|
{
|
||||||
ID: "configmap",
|
ID: "configmap",
|
||||||
Formatter: formatters.DropHelmData,
|
Formatter: formatters.HandleHelmData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "secret",
|
ID: "secret",
|
||||||
Formatter: formatters.DropHelmData,
|
Formatter: formatters.HandleHelmData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "pod",
|
ID: "pod",
|
||||||
@ -83,11 +83,11 @@ func DefaultSchemaTemplatesForStore(store types.Store,
|
|||||||
apigroups.Template(discovery),
|
apigroups.Template(discovery),
|
||||||
{
|
{
|
||||||
ID: "configmap",
|
ID: "configmap",
|
||||||
Formatter: formatters.DropHelmData,
|
Formatter: formatters.HandleHelmData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "secret",
|
ID: "secret",
|
||||||
Formatter: formatters.DropHelmData,
|
Formatter: formatters.HandleHelmData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "pod",
|
ID: "pod",
|
||||||
|
103
pkg/resources/virtual/common/common.go
Normal file
103
pkg/resources/virtual/common/common.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// Package common provides cache.TransformFunc's which are common to all types
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/summarycache"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/data"
|
||||||
|
wranglerSummary "github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SummaryCache provides an interface to get a summary/relationships for an object. Implemented by the summaryCache
|
||||||
|
// struct from pkg/summarycache
|
||||||
|
type SummaryCache interface {
|
||||||
|
SummaryAndRelationship(runtime.Object) (*wranglerSummary.SummarizedObject, []summarycache.Relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultFields produces a VirtualTransformFunc through GetTransform() that applies to all k8s types
|
||||||
|
type DefaultFields struct {
|
||||||
|
Cache SummaryCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformCommon implements virtual.VirtualTransformFunc, and adds reserved fields/summary
|
||||||
|
func (d *DefaultFields) TransformCommon(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||||
|
obj = addIDField(obj)
|
||||||
|
obj, err := addSummaryFields(obj, d.Cache)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to add summary fields: %w", err)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSummaryFields adds the virtual fields for object state.
|
||||||
|
func addSummaryFields(raw *unstructured.Unstructured, cache SummaryCache) (*unstructured.Unstructured, error) {
|
||||||
|
s, relationships := cache.SummaryAndRelationship(raw)
|
||||||
|
if s != nil {
|
||||||
|
data.PutValue(raw.Object, map[string]interface{}{
|
||||||
|
"name": s.State,
|
||||||
|
"error": s.Error,
|
||||||
|
"transitioning": s.Transitioning,
|
||||||
|
"message": strings.Join(s.Message, ":"),
|
||||||
|
}, "metadata", "state")
|
||||||
|
|
||||||
|
}
|
||||||
|
var rels []any
|
||||||
|
for _, relationship := range relationships {
|
||||||
|
rel, err := toMap(relationship)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to convert relationship to map: %w", err)
|
||||||
|
}
|
||||||
|
rels = append(rels, rel)
|
||||||
|
}
|
||||||
|
data.PutValue(raw.Object, rels, "metadata", "relationships")
|
||||||
|
|
||||||
|
normalizeConditions(raw)
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addIDField adds the ID field based on namespace/name, and moves the current id field to _id if present
|
||||||
|
func addIDField(raw *unstructured.Unstructured) *unstructured.Unstructured {
|
||||||
|
objectID := raw.GetName()
|
||||||
|
namespace := raw.GetNamespace()
|
||||||
|
if namespace != "" {
|
||||||
|
objectID = fmt.Sprintf("%s/%s", namespace, objectID)
|
||||||
|
}
|
||||||
|
currentIDValue, ok := raw.Object["id"]
|
||||||
|
if ok {
|
||||||
|
raw.Object["_id"] = currentIDValue
|
||||||
|
}
|
||||||
|
raw.Object["id"] = objectID
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeConditions(raw *unstructured.Unstructured) {
|
||||||
|
var (
|
||||||
|
obj data.Object
|
||||||
|
newConditions []any
|
||||||
|
)
|
||||||
|
|
||||||
|
obj = raw.Object
|
||||||
|
for _, condition := range obj.Slice("status", "conditions") {
|
||||||
|
var summary wranglerSummary.Summary
|
||||||
|
for _, summarizer := range wranglerSummary.ConditionSummarizers {
|
||||||
|
summary = summarizer(obj, []wranglerSummary.Condition{{Object: condition}}, summary)
|
||||||
|
}
|
||||||
|
condition.Set("error", summary.Error)
|
||||||
|
condition.Set("transitioning", summary.Transitioning)
|
||||||
|
|
||||||
|
if condition.String("lastUpdateTime") == "" {
|
||||||
|
condition.Set("lastUpdateTime", condition.String("lastTransitionTime"))
|
||||||
|
}
|
||||||
|
// needs to be reconverted back to a map[string]any or we can have encoding problems with unregistered types
|
||||||
|
var mapCondition map[string]any = condition
|
||||||
|
newConditions = append(newConditions, mapCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newConditions) > 0 {
|
||||||
|
obj.SetNested(newConditions, "status", "conditions")
|
||||||
|
}
|
||||||
|
}
|
187
pkg/resources/virtual/common/common_test.go
Normal file
187
pkg/resources/virtual/common/common_test.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
package common_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
|
"github.com/rancher/steve/pkg/summarycache"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransformCommonObjects(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input any
|
||||||
|
hasSummary *summary.SummarizedObject
|
||||||
|
hasRelationships []summarycache.Relationship
|
||||||
|
wantOutput any
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "signal error",
|
||||||
|
input: cache.DeletedFinalStateUnknown{
|
||||||
|
Key: "some-ns/some-name",
|
||||||
|
},
|
||||||
|
wantOutput: cache.DeletedFinalStateUnknown{
|
||||||
|
Key: "some-ns/some-name",
|
||||||
|
},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not unstructured",
|
||||||
|
input: map[string]any{
|
||||||
|
"somekey": "someval",
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add summary + relationships + reserved fields",
|
||||||
|
hasSummary: &summary.SummarizedObject{
|
||||||
|
PartialObjectMetadata: v1.PartialObjectMetadata{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: "testobj",
|
||||||
|
Namespace: "test-ns",
|
||||||
|
},
|
||||||
|
TypeMeta: v1.TypeMeta{
|
||||||
|
APIVersion: "test.cattle.io/v1",
|
||||||
|
Kind: "TestResource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Summary: summary.Summary{
|
||||||
|
State: "success",
|
||||||
|
Transitioning: false,
|
||||||
|
Error: false,
|
||||||
|
Message: []string{"resource 1 rolled out", "resource 2 rolled out"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasRelationships: []summarycache.Relationship{
|
||||||
|
{
|
||||||
|
ToID: "1345",
|
||||||
|
ToType: "SomeType",
|
||||||
|
ToNamespace: "some-ns",
|
||||||
|
FromID: "78901",
|
||||||
|
FromType: "TestResource",
|
||||||
|
Rel: "uses",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
},
|
||||||
|
"id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"state": map[string]interface{}{
|
||||||
|
"name": "success",
|
||||||
|
"error": false,
|
||||||
|
"transitioning": false,
|
||||||
|
"message": "resource 1 rolled out:resource 2 rolled out",
|
||||||
|
},
|
||||||
|
"relationships": []any{
|
||||||
|
map[string]any{
|
||||||
|
"toId": "1345",
|
||||||
|
"toType": "SomeType",
|
||||||
|
"toNamespace": "some-ns",
|
||||||
|
"fromId": "78901",
|
||||||
|
"fromType": "TestResource",
|
||||||
|
"rel": "uses",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "test-ns/testobj",
|
||||||
|
"_id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add conditions + reserved fields",
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Error",
|
||||||
|
"message": "some error",
|
||||||
|
"lastTransitionTime": "2024-01-01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Error",
|
||||||
|
"transitioning": false,
|
||||||
|
"error": true,
|
||||||
|
"message": "some error",
|
||||||
|
"lastTransitionTime": "2024-01-01",
|
||||||
|
"lastUpdateTime": "2024-01-01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "test-ns/testobj",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
fakeCache := common.FakeSummaryCache{
|
||||||
|
SummarizedObject: test.hasSummary,
|
||||||
|
Relationships: test.hasRelationships,
|
||||||
|
}
|
||||||
|
df := common.DefaultFields{
|
||||||
|
Cache: &fakeCache,
|
||||||
|
}
|
||||||
|
raw, isSignal, err := common.GetUnstructured(test.input)
|
||||||
|
if err != nil {
|
||||||
|
require.True(t, test.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isSignal {
|
||||||
|
require.Equal(t, test.input, test.wantOutput)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, err := df.TransformCommon(raw)
|
||||||
|
if test.wantError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, test.wantOutput, output)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
pkg/resources/virtual/common/testutil.go
Normal file
16
pkg/resources/virtual/common/testutil.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/steve/pkg/summarycache"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeSummaryCache struct {
|
||||||
|
SummarizedObject *summary.SummarizedObject
|
||||||
|
Relationships []summarycache.Relationship
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSummaryCache) SummaryAndRelationship(runtime.Object) (*summary.SummarizedObject, []summarycache.Relationship) {
|
||||||
|
return f.SummarizedObject, f.Relationships
|
||||||
|
}
|
40
pkg/resources/virtual/common/util.go
Normal file
40
pkg/resources/virtual/common/util.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUnstructured retrieves an unstructured object from the provided input. If this is a signal
|
||||||
|
// object (like cache.DeletedFinalStateUnknown), returns true, indicating that this wasn't an
|
||||||
|
// unstructured object, but doesn't need to be processed by our transform function
|
||||||
|
func GetUnstructured(obj any) (*unstructured.Unstructured, bool, error) {
|
||||||
|
raw, ok := obj.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
_, isFinalUnknown := obj.(cache.DeletedFinalStateUnknown)
|
||||||
|
if isFinalUnknown {
|
||||||
|
// As documented in the TransformFunc interface
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
return nil, false, fmt.Errorf("object was of type %T, not unstructured", raw)
|
||||||
|
}
|
||||||
|
return raw, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toMap converts an object to a map[string]any which can be stored/retrieved from the cache. Currently
|
||||||
|
// uses json encoding to take advantage of tag names
|
||||||
|
func toMap(obj any) (map[string]any, error) {
|
||||||
|
bytes, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to marshal object: %w", err)
|
||||||
|
}
|
||||||
|
var retObj map[string]any
|
||||||
|
err = json.Unmarshal(bytes, &retObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to unmarshal object: %w", err)
|
||||||
|
}
|
||||||
|
return retObj, nil
|
||||||
|
}
|
16
pkg/resources/virtual/events/events.go
Normal file
16
pkg/resources/virtual/events/events.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Package common provides cache.TransformFunc's for /v1 Event objects
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransformEventObject does special-case handling on event objects
|
||||||
|
// 1. (only one so far): replaces the _type field with the contents of the field named "type", if it exists
|
||||||
|
func TransformEventObject(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||||
|
currentTypeValue, ok := obj.Object["type"]
|
||||||
|
if ok {
|
||||||
|
obj.Object["_type"] = currentTypeValue
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
93
pkg/resources/virtual/events/events_test.go
Normal file
93
pkg/resources/virtual/events/events_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package events_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual/events"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransformEvents(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input any
|
||||||
|
wantOutput any
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fix event fields",
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
},
|
||||||
|
"id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
},
|
||||||
|
"id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
"_type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "don't fix non-default-group event fields",
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "palau.io/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
},
|
||||||
|
"id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "palau.io/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
},
|
||||||
|
"id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var output interface{}
|
||||||
|
var err error
|
||||||
|
raw, ok := test.input.(*unstructured.Unstructured)
|
||||||
|
if ok && raw.GetKind() == "Event" && raw.GetAPIVersion() == "/v1" {
|
||||||
|
output, err = events.TransformEventObject(raw)
|
||||||
|
} else {
|
||||||
|
output = raw
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
require.Equal(t, test.wantOutput, output)
|
||||||
|
if test.wantError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
55
pkg/resources/virtual/virtual.go
Normal file
55
pkg/resources/virtual/virtual.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Package virtual provides functions/resources to define virtual fields (fields which don't exist in k8s
|
||||||
|
// but should be visible in the API) on resources
|
||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual/events"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransformBuilder builds transform functions for specified GVKs through GetTransformFunc
|
||||||
|
type TransformBuilder struct {
|
||||||
|
defaultFields *common.DefaultFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransformBuilder returns a TransformBuilder using the given summary cache
|
||||||
|
func NewTransformBuilder(cache common.SummaryCache) *TransformBuilder {
|
||||||
|
return &TransformBuilder{
|
||||||
|
defaultFields: &common.DefaultFields{
|
||||||
|
Cache: cache,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransformFunc returns the func to transform a raw object into a fixed object, if needed
|
||||||
|
func (t *TransformBuilder) GetTransformFunc(gvk schema.GroupVersionKind) cache.TransformFunc {
|
||||||
|
converters := make([]func(*unstructured.Unstructured) (*unstructured.Unstructured, error), 0)
|
||||||
|
if gvk.Kind == "Event" && gvk.Group == "" && gvk.Version == "v1" {
|
||||||
|
converters = append(converters, events.TransformEventObject)
|
||||||
|
}
|
||||||
|
converters = append(converters, t.defaultFields.TransformCommon)
|
||||||
|
|
||||||
|
return func(raw interface{}) (interface{}, error) {
|
||||||
|
obj, isSignal, err := common.GetUnstructured(raw)
|
||||||
|
if isSignal {
|
||||||
|
// isSignal= true overrides any error
|
||||||
|
return raw, err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetUnstructured: failed to get underlying object: %w", err)
|
||||||
|
}
|
||||||
|
// Conversions are run in this loop:
|
||||||
|
for _, f := range converters {
|
||||||
|
obj, err = f(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
}
|
202
pkg/resources/virtual/virtual_test.go
Normal file
202
pkg/resources/virtual/virtual_test.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package virtual_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
|
"github.com/rancher/steve/pkg/summarycache"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransformChain(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input any
|
||||||
|
hasSummary *summary.SummarizedObject
|
||||||
|
hasRelationships []summarycache.Relationship
|
||||||
|
wantOutput any
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add summary + relationships + reserved fields",
|
||||||
|
hasSummary: &summary.SummarizedObject{
|
||||||
|
PartialObjectMetadata: v1.PartialObjectMetadata{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: "testobj",
|
||||||
|
Namespace: "test-ns",
|
||||||
|
},
|
||||||
|
TypeMeta: v1.TypeMeta{
|
||||||
|
APIVersion: "test.cattle.io/v1",
|
||||||
|
Kind: "TestResource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Summary: summary.Summary{
|
||||||
|
State: "success",
|
||||||
|
Transitioning: false,
|
||||||
|
Error: false,
|
||||||
|
Message: []string{"resource 1 rolled out", "resource 2 rolled out"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasRelationships: []summarycache.Relationship{
|
||||||
|
{
|
||||||
|
ToID: "1345",
|
||||||
|
ToType: "SomeType",
|
||||||
|
ToNamespace: "some-ns",
|
||||||
|
FromID: "78901",
|
||||||
|
FromType: "TestResource",
|
||||||
|
Rel: "uses",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
},
|
||||||
|
"id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"state": map[string]interface{}{
|
||||||
|
"name": "success",
|
||||||
|
"error": false,
|
||||||
|
"transitioning": false,
|
||||||
|
"message": "resource 1 rolled out:resource 2 rolled out",
|
||||||
|
},
|
||||||
|
"relationships": []any{
|
||||||
|
map[string]any{
|
||||||
|
"toId": "1345",
|
||||||
|
"toType": "SomeType",
|
||||||
|
"toNamespace": "some-ns",
|
||||||
|
"fromId": "78901",
|
||||||
|
"fromType": "TestResource",
|
||||||
|
"rel": "uses",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "test-ns/testobj",
|
||||||
|
"_id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "processable event",
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "oswaldsFarm",
|
||||||
|
"namespace": "oswaldsNamespace",
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Error",
|
||||||
|
"message": "some error",
|
||||||
|
"lastTransitionTime": "2024-01-01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "eventTest2id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "oswaldsFarm",
|
||||||
|
"namespace": "oswaldsNamespace",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
},
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"conditions": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"status": "False",
|
||||||
|
"reason": "Error",
|
||||||
|
"transitioning": false,
|
||||||
|
"error": true,
|
||||||
|
"message": "some error",
|
||||||
|
"lastTransitionTime": "2024-01-01",
|
||||||
|
"lastUpdateTime": "2024-01-01",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "oswaldsNamespace/oswaldsFarm",
|
||||||
|
"_id": "eventTest2id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
"_type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "don't fix non-default-group event fields",
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "palau.io/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
},
|
||||||
|
"id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "palau.io/v1",
|
||||||
|
"kind": "Event",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "gregsFarm",
|
||||||
|
"namespace": "gregsNamespace",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
},
|
||||||
|
"id": "gregsNamespace/gregsFarm",
|
||||||
|
"_id": "eventTest1id",
|
||||||
|
"type": "Gorniplatz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
fakeCache := common.FakeSummaryCache{
|
||||||
|
SummarizedObject: test.hasSummary,
|
||||||
|
Relationships: test.hasRelationships,
|
||||||
|
}
|
||||||
|
tb := virtual.NewTransformBuilder(&fakeCache)
|
||||||
|
raw, isSignal, err := common.GetUnstructured(test.input)
|
||||||
|
require.False(t, isSignal)
|
||||||
|
require.Nil(t, err)
|
||||||
|
apiVersion := raw.GetAPIVersion()
|
||||||
|
parts := strings.Split(apiVersion, "/")
|
||||||
|
gvk := schema.GroupVersionKind{Group: parts[0], Version: parts[1], Kind: raw.GetKind()}
|
||||||
|
output, err := tb.GetTransformFunc(gvk)(test.input)
|
||||||
|
require.Equal(t, test.wantOutput, output)
|
||||||
|
if test.wantError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -63,6 +63,26 @@ spec:
|
|||||||
nullable: true
|
nullable: true
|
||||||
served: true
|
served: true
|
||||||
storage: true
|
storage: true
|
||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: schemaless.management.cattle.io
|
||||||
|
spec:
|
||||||
|
conversion:
|
||||||
|
strategy: None
|
||||||
|
group: management.cattle.io
|
||||||
|
names:
|
||||||
|
kind: Schemaless
|
||||||
|
listKind: SchemalessList
|
||||||
|
plural: schemalese
|
||||||
|
singular: schemaless
|
||||||
|
scope: Cluster
|
||||||
|
preserveUnkownFields: true
|
||||||
|
versions:
|
||||||
|
- name: v2
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -346,6 +366,35 @@ definitions:
|
|||||||
- group: "management.cattle.io"
|
- group: "management.cattle.io"
|
||||||
version: "v2"
|
version: "v2"
|
||||||
kind: "Nullable"
|
kind: "Nullable"
|
||||||
|
io.cattle.management.v2.Schemaless:
|
||||||
|
description: "this kind has no schema"
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: "The APIVersion of this resource"
|
||||||
|
type: "string"
|
||||||
|
kind:
|
||||||
|
description: "The kind"
|
||||||
|
type: "string"
|
||||||
|
metadata:
|
||||||
|
description: "The metadata"
|
||||||
|
$ref: "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
||||||
|
spec:
|
||||||
|
description: "The spec for the resource"
|
||||||
|
type: "object"
|
||||||
|
required:
|
||||||
|
- "name"
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: "The name of the resource"
|
||||||
|
type: "string"
|
||||||
|
notRequired:
|
||||||
|
description: "Some field that isn't required"
|
||||||
|
type: "boolean"
|
||||||
|
x-kubernetes-group-version-kind:
|
||||||
|
- group: "management.cattle.io"
|
||||||
|
version: "v2"
|
||||||
|
kind: "Schemaless"
|
||||||
io.cattle.management.NotAKind:
|
io.cattle.management.NotAKind:
|
||||||
type: "string"
|
type: "string"
|
||||||
description: "Some string which isn't a kind"
|
description: "Some string which isn't a kind"
|
||||||
|
@ -223,7 +223,9 @@ func listGVKModels(models proto.Models, groups *metav1.APIGroupList, crdCache wa
|
|||||||
Version: version.Name,
|
Version: version.Name,
|
||||||
Kind: crd.Spec.Names.Kind,
|
Kind: crd.Spec.Names.Kind,
|
||||||
}
|
}
|
||||||
gvkToCRD[gvk] = version.Schema.OpenAPIV3Schema
|
if version.Schema != nil {
|
||||||
|
gvkToCRD[gvk] = version.Schema.OpenAPIV3Schema
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ func TestRefresh(t *testing.T) {
|
|||||||
require.NotNil(t, userAttributesV2)
|
require.NotNil(t, userAttributesV2)
|
||||||
|
|
||||||
nullableV2 := getJSONSchema(crds, "nullable.management.cattle.io", "v2")
|
nullableV2 := getJSONSchema(crds, "nullable.management.cattle.io", "v2")
|
||||||
require.NotNil(t, userAttributesV2)
|
require.NotNil(t, nullableV2)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -85,6 +85,11 @@ func TestRefresh(t *testing.T) {
|
|||||||
Schema: defaultModels.LookupModel("io.cattle.management.v2.Nullable"),
|
Schema: defaultModels.LookupModel("io.cattle.management.v2.Nullable"),
|
||||||
CRD: nullableV2,
|
CRD: nullableV2,
|
||||||
},
|
},
|
||||||
|
"management.cattle.io.schemaless": {
|
||||||
|
ModelName: "io.cattle.management.v2.Schemaless",
|
||||||
|
Schema: defaultModels.LookupModel("io.cattle.management.v2.Schemaless"),
|
||||||
|
CRD: nil,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -147,6 +152,11 @@ func TestRefresh(t *testing.T) {
|
|||||||
Schema: defaultModels.LookupModel("io.cattle.management.v2.Nullable"),
|
Schema: defaultModels.LookupModel("io.cattle.management.v2.Nullable"),
|
||||||
CRD: nullableV2,
|
CRD: nullableV2,
|
||||||
},
|
},
|
||||||
|
"management.cattle.io.schemaless": {
|
||||||
|
ModelName: "io.cattle.management.v2.Schemaless",
|
||||||
|
Schema: defaultModels.LookupModel("io.cattle.management.v2.Schemaless"),
|
||||||
|
CRD: nil,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func setup(ctx context.Context, server *Server) error {
|
|||||||
|
|
||||||
var onSchemasHandler schemacontroller.SchemasHandlerFunc
|
var onSchemasHandler schemacontroller.SchemasHandlerFunc
|
||||||
if server.SQLCache {
|
if server.SQLCache {
|
||||||
s, err := sqlproxy.NewProxyStore(cols, cf, summaryCache, nil)
|
s, err := sqlproxy.NewProxyStore(cols, cf, summaryCache, summaryCache, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ package partition
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -110,7 +111,7 @@ func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return ToAPI(schema, obj, warnings), nil
|
return ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByID looks up a single object by its ID.
|
// ByID looks up a single object by its ID.
|
||||||
@ -124,7 +125,7 @@ func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return ToAPI(schema, obj, warnings), nil
|
return ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) listPartition(ctx context.Context, apiOp *types.APIRequest, schema *types.APISchema, partition Partition,
|
func (s *Store) listPartition(ctx context.Context, apiOp *types.APIRequest, schema *types.APISchema, partition Partition,
|
||||||
@ -226,7 +227,7 @@ func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.AP
|
|||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
item := item.DeepCopy()
|
item := item.DeepCopy()
|
||||||
result.Objects = append(result.Objects, ToAPI(schema, item, nil))
|
result.Objects = append(result.Objects, ToAPI(schema, item, nil, types.ReservedFields))
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Pages = pages
|
result.Pages = pages
|
||||||
@ -266,7 +267,7 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data ty
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return ToAPI(schema, obj, warnings), nil
|
return ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a single object in the store.
|
// Update updates a single object in the store.
|
||||||
@ -280,7 +281,7 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data ty
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return ToAPI(schema, obj, warnings), nil
|
return ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch returns a channel of events for a list or resource.
|
// Watch returns a channel of events for a list or resource.
|
||||||
@ -326,13 +327,13 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToAPI(schema *types.APISchema, obj runtime.Object, warnings []types.Warning) types.APIObject {
|
func ToAPI(schema *types.APISchema, obj runtime.Object, warnings []types.Warning, reservedFields map[string]bool) types.APIObject {
|
||||||
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
||||||
return types.APIObject{}
|
return types.APIObject{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if unstr, ok := obj.(*unstructured.Unstructured); ok {
|
if unstr, ok := obj.(*unstructured.Unstructured); ok {
|
||||||
obj = moveToUnderscore(unstr)
|
obj = moveToUnderscore(unstr, reservedFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiObject := types.APIObject{
|
apiObject := types.APIObject{
|
||||||
@ -356,12 +357,12 @@ func ToAPI(schema *types.APISchema, obj runtime.Object, warnings []types.Warning
|
|||||||
return apiObject
|
return apiObject
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveToUnderscore(obj *unstructured.Unstructured) *unstructured.Unstructured {
|
func moveToUnderscore(obj *unstructured.Unstructured, reservedFields map[string]bool) *unstructured.Unstructured {
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range types.ReservedFields {
|
for k := range reservedFields {
|
||||||
v, ok := obj.Object[k]
|
v, ok := obj.Object[k]
|
||||||
if ok {
|
if ok {
|
||||||
delete(obj.Object, k)
|
delete(obj.Object, k)
|
||||||
@ -389,11 +390,11 @@ func ToAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, event watch.Ev
|
|||||||
|
|
||||||
if event.Type == watch.Error {
|
if event.Type == watch.Error {
|
||||||
status, _ := event.Object.(*metav1.Status)
|
status, _ := event.Object.(*metav1.Status)
|
||||||
apiEvent.Error = fmt.Errorf(status.Message)
|
apiEvent.Error = errors.New(status.Message)
|
||||||
return apiEvent
|
return apiEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEvent.Object = ToAPI(schema, event.Object, nil)
|
apiEvent.Object = ToAPI(schema, event.Object, nil, types.ReservedFields)
|
||||||
|
|
||||||
m, err := meta.Accessor(event.Object)
|
m, err := meta.Accessor(event.Object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -13,15 +13,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
|
||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
|
||||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
|
||||||
"github.com/rancher/steve/pkg/stores/partition"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/data"
|
|
||||||
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
@ -34,9 +25,25 @@ import (
|
|||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
|
"github.com/rancher/apiserver/pkg/apierror"
|
||||||
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/data"
|
||||||
|
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
|
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||||
|
"github.com/rancher/steve/pkg/stores/partition"
|
||||||
)
|
)
|
||||||
|
|
||||||
const watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
|
const (
|
||||||
|
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
|
||||||
|
errNamespaceRequired = "metadata.namespace is required"
|
||||||
|
errResourceVersionRequired = "metadata.resourceVersion is required for update"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lowerChars = regexp.MustCompile("[a-z]+")
|
lowerChars = regexp.MustCompile("[a-z]+")
|
||||||
@ -422,20 +429,27 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params
|
|||||||
}
|
}
|
||||||
|
|
||||||
name := types.Name(input)
|
name := types.Name(input)
|
||||||
ns := types.Namespace(input)
|
namespace := types.Namespace(input)
|
||||||
if name == "" && input.String("metadata", "generateName") == "" {
|
generateName := input.String("metadata", "generateName")
|
||||||
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
|
|
||||||
|
if name == "" && generateName == "" {
|
||||||
|
input.SetNested(schema.ID[0:1]+"-", "metadata", "generateName")
|
||||||
}
|
}
|
||||||
if ns == "" && apiOp.Namespace != "" {
|
|
||||||
ns = apiOp.Namespace
|
if attributes.Namespaced(schema) && namespace == "" {
|
||||||
input.SetNested(ns, "metadata", "namespace")
|
if apiOp.Namespace == "" {
|
||||||
|
return nil, nil, apierror.NewAPIError(validation.InvalidBodyContent, errNamespaceRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = apiOp.Namespace
|
||||||
|
input.SetNested(namespace, "metadata", "namespace")
|
||||||
}
|
}
|
||||||
|
|
||||||
gvk := attributes.GVK(schema)
|
gvk := attributes.GVK(schema)
|
||||||
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||||
|
|
||||||
buffer := WarningBuffer{}
|
buffer := WarningBuffer{}
|
||||||
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer))
|
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, namespace, &buffer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -502,9 +516,12 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params
|
|||||||
|
|
||||||
resourceVersion := input.String("metadata", "resourceVersion")
|
resourceVersion := input.String("metadata", "resourceVersion")
|
||||||
if resourceVersion == "" {
|
if resourceVersion == "" {
|
||||||
return nil, nil, fmt.Errorf("metadata.resourceVersion is required for update")
|
return nil, nil, errors.New(errResourceVersionRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gvk := attributes.GVK(schema)
|
||||||
|
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||||
|
|
||||||
opts := metav1.UpdateOptions{}
|
opts := metav1.UpdateOptions{}
|
||||||
if err := decodeParams(apiOp, &opts); err != nil {
|
if err := decodeParams(apiOp, &opts); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -2,26 +2,35 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
|
||||||
"github.com/rancher/steve/pkg/client"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
"k8s.io/client-go/dynamic/fake"
|
"k8s.io/client-go/dynamic/fake"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
clientgotesting "k8s.io/client-go/testing"
|
clientgotesting "k8s.io/client-go/testing"
|
||||||
|
|
||||||
|
"github.com/rancher/apiserver/pkg/apierror"
|
||||||
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
var c *watch.FakeWatcher
|
var c *watch.FakeWatcher
|
||||||
@ -32,6 +41,14 @@ type testFactory struct {
|
|||||||
fakeClient *fake.FakeDynamicClient
|
fakeClient *fake.FakeDynamicClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testFactory) TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
|
||||||
|
return t.fakeClient.Resource(schema2.GroupVersionResource{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testFactory) TableClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
|
||||||
|
return t.fakeClient.Resource(schema2.GroupVersionResource{}).Namespace(namespace), nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestWatchNamesErrReceive(t *testing.T) {
|
func TestWatchNamesErrReceive(t *testing.T) {
|
||||||
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
|
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -80,10 +97,6 @@ func TestByNames(t *testing.T) {
|
|||||||
assert.Nil(t, warn)
|
assert.Nil(t, warn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testFactory) TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error) {
|
|
||||||
return t.fakeClient.Resource(schema2.GroupVersionResource{}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveUntil(wc chan watch.Event, d time.Duration) error {
|
func receiveUntil(wc chan watch.Event, d time.Duration) error {
|
||||||
timer := time.NewTicker(d)
|
timer := time.NewTicker(d)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
@ -121,3 +134,711 @@ func receiveUntil(wc chan watch.Event, d time.Duration) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
type input struct {
|
||||||
|
apiOp *types.APIRequest
|
||||||
|
schema *types.APISchema
|
||||||
|
params types.APIObject
|
||||||
|
}
|
||||||
|
|
||||||
|
type expected struct {
|
||||||
|
value *unstructured.Unstructured
|
||||||
|
warning []types.Warning
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
namespace string
|
||||||
|
input input
|
||||||
|
expected expected
|
||||||
|
createReactorFunc clientgotesting.ReactionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "creating resource - namespace scoped",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "creating resource - cluster scoped",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing name",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"generateName": "testing-gen-name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"generateName": "testing-gen-name",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing name / generateName",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"generateName": "t-",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing namespace in the params (should copy from apiOp)",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Namespace: "testing-ns",
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing namespace - namespace scoped",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: nil,
|
||||||
|
warning: nil,
|
||||||
|
err: apierror.NewAPIError(
|
||||||
|
validation.InvalidBodyContent,
|
||||||
|
errNamespaceRequired,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error response",
|
||||||
|
input: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Request: &http.Request{URL: &url.URL{}},
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
createReactorFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return true, nil, apierrors.NewUnauthorized("sample reason")
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: nil,
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: apierrors.NewUnauthorized("sample reason"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
||||||
|
|
||||||
|
if tt.createReactorFunc != nil {
|
||||||
|
fakeClient.PrependReactor("create", "*", tt.createReactorFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
testStore := Store{
|
||||||
|
clientGetter: &testFactory{Factory: testClientFactory,
|
||||||
|
fakeClient: fakeClient,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
value, warning, err := testStore.Create(tt.input.apiOp, tt.input.schema, tt.input.params)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected.value, value)
|
||||||
|
assert.Equal(t, tt.expected.warning, warning)
|
||||||
|
assert.Equal(t, tt.expected.err, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
type input struct {
|
||||||
|
apiOp *types.APIRequest
|
||||||
|
schema *types.APISchema
|
||||||
|
params types.APIObject
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
type expected struct {
|
||||||
|
value *unstructured.Unstructured
|
||||||
|
warning []types.Warning
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleCreateInput := input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPost,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPost,
|
||||||
|
},
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"kind": "Secret",
|
||||||
|
"version": "v1",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"kind": "Secret",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
updateCallbackFunc clientgotesting.ReactionFunc
|
||||||
|
createInput *input
|
||||||
|
updateInput input
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "update - usual request",
|
||||||
|
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
createInput: &sampleCreateInput,
|
||||||
|
updateInput: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update - different apiVersion and kind (params and schema) - should copy from schema",
|
||||||
|
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
createInput: &sampleCreateInput,
|
||||||
|
updateInput: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "ConfigMap",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update - missing apiVersion and kind in params - should copy from schema",
|
||||||
|
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
createInput: &sampleCreateInput,
|
||||||
|
updateInput: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPost,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPost,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
warning: []types.Warning{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update - missing resource version",
|
||||||
|
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return false, ret, nil
|
||||||
|
},
|
||||||
|
updateInput: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"version": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Secret",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: nil,
|
||||||
|
warning: nil,
|
||||||
|
err: errors.New(errResourceVersionRequired),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update - error request",
|
||||||
|
updateCallbackFunc: func(action clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||||
|
return true, nil, apierrors.NewUnauthorized("sample reason")
|
||||||
|
},
|
||||||
|
createInput: &sampleCreateInput,
|
||||||
|
updateInput: input{
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Request: &http.Request{
|
||||||
|
URL: &url.URL{},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
Schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Method: http.MethodPut,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "testing",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"kind": "Secret",
|
||||||
|
"namespaced": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: types.APIObject{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v2",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testing-secret",
|
||||||
|
"namespace": "testing-ns",
|
||||||
|
"resourceVersion": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
value: nil,
|
||||||
|
warning: nil,
|
||||||
|
err: apierrors.NewUnauthorized("sample reason"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
|
||||||
|
|
||||||
|
if tt.updateCallbackFunc != nil {
|
||||||
|
fakeClient.PrependReactor("update", "*", tt.updateCallbackFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
testStore := Store{
|
||||||
|
clientGetter: &testFactory{Factory: testClientFactory,
|
||||||
|
fakeClient: fakeClient,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating the object first, so we can update it later (this function is not the SUT)
|
||||||
|
if tt.createInput != nil {
|
||||||
|
_, _, err = testStore.Create(tt.createInput.apiOp, tt.createInput.schema, tt.createInput.params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, warning, err := testStore.Update(tt.updateInput.apiOp, tt.updateInput.schema, tt.updateInput.params, tt.updateInput.id)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expected.value, value)
|
||||||
|
assert.Equal(t, tt.expected.warning, warning)
|
||||||
|
|
||||||
|
if tt.expected.err != nil {
|
||||||
|
assert.Equal(t, tt.expected.err.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/steve/pkg/stores/partition"
|
"github.com/rancher/steve/pkg/stores/partition"
|
||||||
"github.com/rancher/wrangler/v3/pkg/kv"
|
"github.com/rancher/wrangler/v3/pkg/kv"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
@ -64,17 +65,10 @@ func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema,
|
|||||||
fallthrough
|
fallthrough
|
||||||
case "watch":
|
case "watch":
|
||||||
if id != "" {
|
if id != "" {
|
||||||
ns, name := kv.RSplit(id, "/")
|
partitions := generatePartitionsByID(apiOp, schema, verb, id)
|
||||||
return []partition.Partition{
|
return partitions, nil
|
||||||
Partition{
|
|
||||||
Namespace: ns,
|
|
||||||
All: false,
|
|
||||||
Passthrough: false,
|
|
||||||
Names: sets.NewString(name),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
partitions, passthrough := isPassthrough(apiOp, schema, verb)
|
partitions, passthrough := generateAggregatePartitions(apiOp, schema, verb)
|
||||||
if passthrough {
|
if passthrough {
|
||||||
return passthroughPartitions, nil
|
return passthroughPartitions, nil
|
||||||
}
|
}
|
||||||
@ -126,15 +120,92 @@ func (b *byNameOrNamespaceStore) Watch(apiOp *types.APIRequest, schema *types.AP
|
|||||||
return b.Store.WatchNames(apiOp, schema, wr, b.partition.Names)
|
return b.Store.WatchNames(apiOp, schema, wr, b.partition.Names)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPassthrough determines whether a request can be passed through directly to the underlying store
|
// generatePartitionsById determines whether a requester can access a particular resource
|
||||||
// or if the results need to be partitioned by namespace and name based on the requester's access.
|
// and if so, returns the corresponding partitions
|
||||||
func isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
func generatePartitionsByID(apiOp *types.APIRequest, schema *types.APISchema, verb string, id string) []partition.Partition {
|
||||||
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
resources := accessListByVerb.Granted(verb)
|
||||||
|
|
||||||
|
idNamespace, name := kv.RSplit(id, "/")
|
||||||
|
apiNamespace := apiOp.Namespace
|
||||||
|
effectiveNamespace := idNamespace
|
||||||
|
|
||||||
|
// If a non-empty namespace was provided, be sure to select that for filtering and permissions checks
|
||||||
|
if idNamespace == "" && apiNamespace != "" {
|
||||||
|
effectiveNamespace = apiNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// The external API is flexible, and permits specifying a namespace as a separate key or embedded
|
||||||
|
// within the ID of the object. Both of these cases should be valid:
|
||||||
|
// {"namespace": "n1", "id": "r1"}
|
||||||
|
// {"id": "n1/r1"}
|
||||||
|
// however, the following conflicting request is not valid, but was previously accepted:
|
||||||
|
// {"namespace": "n1", "id": "n2/r1"}
|
||||||
|
// To avoid breaking UI plugins that may inadvertently rely on the feature, we issue a deprecation
|
||||||
|
// warning for now. We still need to pick one of the namespaces for permission verification purposes.
|
||||||
|
if idNamespace != "" && apiNamespace != "" && idNamespace != apiNamespace {
|
||||||
|
logrus.Warningf("DEPRECATION: Conflicting namespaces '%v' and '%v' requested. "+
|
||||||
|
"Selecting '%v' as the effective namespace. Future steve versions will reject this request.",
|
||||||
|
idNamespace, apiNamespace, effectiveNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessListByVerb.All(verb) {
|
||||||
|
return []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.NewString(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if effectiveNamespace != "" {
|
||||||
|
if resources[effectiveNamespace].All {
|
||||||
|
return []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.NewString(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cluster-scoped resources, we will have parsed a "" out
|
||||||
|
// of the ID field from RSplit, but accessListByVerb specifies "*" for
|
||||||
|
// the namespace, so correct that here
|
||||||
|
resourceNamespace := effectiveNamespace
|
||||||
|
if resourceNamespace == "" {
|
||||||
|
resourceNamespace = accesscontrol.All
|
||||||
|
}
|
||||||
|
|
||||||
|
nameset, ok := resources[resourceNamespace]
|
||||||
|
if ok && nameset.Names.Has(name) {
|
||||||
|
return []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.NewString(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAggregatePartitions determines whether a request can be passed through directly to the underlying store
|
||||||
|
// or if the results need to be partitioned by namespace and name based on the requester's access.
|
||||||
|
func generateAggregatePartitions(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
||||||
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
resources := accessListByVerb.Granted(verb)
|
||||||
|
|
||||||
if accessListByVerb.All(verb) {
|
if accessListByVerb.All(verb) {
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
resources := accessListByVerb.Granted(verb)
|
|
||||||
if apiOp.Namespace != "" {
|
if apiOp.Namespace != "" {
|
||||||
if resources[apiOp.Namespace].All {
|
if resources[apiOp.Namespace].All {
|
||||||
return nil, true
|
return nil, true
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAll(t *testing.T) {
|
func TestVerbList(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
apiOp *types.APIRequest
|
apiOp *types.APIRequest
|
||||||
@ -223,7 +223,7 @@ func TestAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "by id",
|
name: "by id fully unauthorized",
|
||||||
apiOp: &types.APIRequest{},
|
apiOp: &types.APIRequest{},
|
||||||
id: "n1/r1",
|
id: "n1/r1",
|
||||||
schema: &types.APISchema{
|
schema: &types.APISchema{
|
||||||
@ -231,6 +231,72 @@ func TestAll(t *testing.T) {
|
|||||||
ID: "foo",
|
ID: "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing namespace",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing resource",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
wantPartitions: []partition.Partition{
|
wantPartitions: []partition.Partition{
|
||||||
Partition{
|
Partition{
|
||||||
Namespace: "n1",
|
Namespace: "n1",
|
||||||
@ -238,8 +304,227 @@ func TestAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by namespaced id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note: this is deprecated fallback behavior. When we remove the behavior,
|
||||||
|
// rewrite this test to expect an error instead.
|
||||||
|
{
|
||||||
|
name: "by id prefers id embedded namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n2",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized globally",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
partitioner := rbacPartitioner{}
|
partitioner := rbacPartitioner{}
|
||||||
@ -250,3 +535,325 @@ func TestAll(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerbWatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiOp *types.APIRequest
|
||||||
|
id string
|
||||||
|
schema *types.APISchema
|
||||||
|
wantPartitions []partition.Partition
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "by id fully unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing namespace",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing resource",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by namespaced id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note: this is deprecated fallback behavior. When we remove the behavior,
|
||||||
|
// rewrite this test to expect an error instead.
|
||||||
|
{
|
||||||
|
name: "by id prefers id embedded namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n2",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.NewString("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized globally",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
Partition{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.NewString("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
partitioner := rbacPartitioner{}
|
||||||
|
verb := "watch"
|
||||||
|
gotPartitions, gotErr := partitioner.All(test.apiOp, test.schema, verb, test.id)
|
||||||
|
assert.Nil(t, gotErr)
|
||||||
|
assert.Equal(t, test.wantPartitions, gotPartitions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/wrangler/v3/pkg/kv"
|
"github.com/rancher/wrangler/v3/pkg/kv"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
@ -46,17 +47,10 @@ func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema,
|
|||||||
fallthrough
|
fallthrough
|
||||||
case "watch":
|
case "watch":
|
||||||
if id != "" {
|
if id != "" {
|
||||||
ns, name := kv.RSplit(id, "/")
|
partitions := generatePartitionsByID(apiOp, schema, verb, id)
|
||||||
return []partition.Partition{
|
return partitions, nil
|
||||||
{
|
|
||||||
Namespace: ns,
|
|
||||||
All: false,
|
|
||||||
Passthrough: false,
|
|
||||||
Names: sets.New[string](name),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
partitions, passthrough := isPassthrough(apiOp, schema, verb)
|
partitions, passthrough := generateAggregatePartitions(apiOp, schema, verb)
|
||||||
if passthrough {
|
if passthrough {
|
||||||
return passthroughPartitions, nil
|
return passthroughPartitions, nil
|
||||||
}
|
}
|
||||||
@ -74,15 +68,92 @@ func (p *rbacPartitioner) Store() UnstructuredStore {
|
|||||||
return p.proxyStore
|
return p.proxyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPassthrough determines whether a request can be passed through directly to the underlying store
|
// generatePartitionsById determines whether a requester can access a particular resource
|
||||||
// or if the results need to be partitioned by namespace and name based on the requester's access.
|
// and if so, returns the corresponding partitions
|
||||||
func isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
func generatePartitionsByID(apiOp *types.APIRequest, schema *types.APISchema, verb string, id string) []partition.Partition {
|
||||||
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
resources := accessListByVerb.Granted(verb)
|
||||||
|
|
||||||
|
idNamespace, name := kv.RSplit(id, "/")
|
||||||
|
apiNamespace := apiOp.Namespace
|
||||||
|
effectiveNamespace := idNamespace
|
||||||
|
|
||||||
|
// If a non-empty namespace was provided, be sure to select that for filtering and permissions checks
|
||||||
|
if idNamespace == "" && apiNamespace != "" {
|
||||||
|
effectiveNamespace = apiNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// The external API is flexible, and permits specifying a namespace as a separate key or embedded
|
||||||
|
// within the ID of the object. Both of these cases should be valid:
|
||||||
|
// {"namespace": "n1", "id": "r1"}
|
||||||
|
// {"id": "n1/r1"}
|
||||||
|
// however, the following conflicting request is not valid, but was previously accepted:
|
||||||
|
// {"namespace": "n1", "id": "n2/r1"}
|
||||||
|
// To avoid breaking UI plugins that may inadvertently rely on the feature, we issue a deprecation
|
||||||
|
// warning for now. We still need to pick one of the namespaces for permission verification purposes.
|
||||||
|
if idNamespace != "" && apiNamespace != "" && idNamespace != apiNamespace {
|
||||||
|
logrus.Warningf("DEPRECATION: Conflicting namespaces '%v' and '%v' requested. "+
|
||||||
|
"Selecting '%v' as the effective namespace. Future steve versions will reject this request.",
|
||||||
|
idNamespace, apiNamespace, effectiveNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessListByVerb.All(verb) {
|
||||||
|
return []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.New(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if effectiveNamespace != "" {
|
||||||
|
if resources[effectiveNamespace].All {
|
||||||
|
return []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.New(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cluster-scoped resources, we will have parsed a "" out
|
||||||
|
// of the ID field from RSplit, but accessListByVerb specifies "*" for
|
||||||
|
// the nameset, so correct that here
|
||||||
|
resourceNamespace := effectiveNamespace
|
||||||
|
if resourceNamespace == "" {
|
||||||
|
resourceNamespace = accesscontrol.All
|
||||||
|
}
|
||||||
|
|
||||||
|
nameset, ok := resources[resourceNamespace]
|
||||||
|
if ok && nameset.Names.Has(name) {
|
||||||
|
return []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: effectiveNamespace,
|
||||||
|
All: false,
|
||||||
|
Passthrough: false,
|
||||||
|
Names: sets.New(name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAggregatePartitions determines whether a request can be passed through directly to the underlying store
|
||||||
|
// or if the results need to be partitioned by namespace and name based on the requester's access.
|
||||||
|
func generateAggregatePartitions(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
|
||||||
|
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
|
||||||
|
resources := accessListByVerb.Granted(verb)
|
||||||
|
|
||||||
if accessListByVerb.All(verb) {
|
if accessListByVerb.All(verb) {
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
resources := accessListByVerb.Granted(verb)
|
|
||||||
if apiOp.Namespace != "" {
|
if apiOp.Namespace != "" {
|
||||||
if resources[apiOp.Namespace].All {
|
if resources[apiOp.Namespace].All {
|
||||||
return nil, true
|
return nil, true
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAll(t *testing.T) {
|
func TestVerbList(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
apiOp *types.APIRequest
|
apiOp *types.APIRequest
|
||||||
@ -225,7 +225,7 @@ func TestAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "by id",
|
name: "by id fully unauthorized",
|
||||||
apiOp: &types.APIRequest{},
|
apiOp: &types.APIRequest{},
|
||||||
id: "n1/r1",
|
id: "n1/r1",
|
||||||
schema: &types.APISchema{
|
schema: &types.APISchema{
|
||||||
@ -233,6 +233,72 @@ func TestAll(t *testing.T) {
|
|||||||
ID: "foo",
|
ID: "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing namespace",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing resource",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
wantPartitions: []partition.Partition{
|
wantPartitions: []partition.Partition{
|
||||||
{
|
{
|
||||||
Namespace: "n1",
|
Namespace: "n1",
|
||||||
@ -240,6 +306,228 @@ func TestAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
All: false,
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by namespaced id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
All: false,
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note: this is deprecated fallback behavior. When we remove the behavior,
|
||||||
|
// rewrite this test to expect an error instead.
|
||||||
|
{
|
||||||
|
name: "by id prefers id embedded namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n2",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized globally",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"list": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -253,6 +541,330 @@ func TestAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerbWatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiOp *types.APIRequest
|
||||||
|
id string
|
||||||
|
schema *types.APISchema
|
||||||
|
wantPartitions []partition.Partition
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "by id fully unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing namespace",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id missing resource",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id authorized by namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
All: false,
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by namespaced id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n1",
|
||||||
|
},
|
||||||
|
id: "r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
All: false,
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "by id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note: this is deprecated fallback behavior. When we remove the behavior,
|
||||||
|
// rewrite this test to expect an error instead.
|
||||||
|
{
|
||||||
|
name: "by id prefers id embedded namespace",
|
||||||
|
apiOp: &types.APIRequest{
|
||||||
|
Namespace: "n2",
|
||||||
|
},
|
||||||
|
id: "n1/r1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": true,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n1",
|
||||||
|
ResourceName: "r1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "n2",
|
||||||
|
ResourceName: "r2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "n1",
|
||||||
|
Names: sets.New[string]("r1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id unauthorized",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized by name",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id authorized globally",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster scoped id ignores unrequested resources",
|
||||||
|
apiOp: &types.APIRequest{},
|
||||||
|
id: "c1",
|
||||||
|
schema: &types.APISchema{
|
||||||
|
Schema: &schemas.Schema{
|
||||||
|
ID: "foo",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"namespaced": false,
|
||||||
|
"access": accesscontrol.AccessListByVerb{
|
||||||
|
"watch": accesscontrol.AccessList{
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c1",
|
||||||
|
},
|
||||||
|
accesscontrol.Access{
|
||||||
|
Namespace: "*",
|
||||||
|
ResourceName: "c2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantPartitions: []partition.Partition{
|
||||||
|
{
|
||||||
|
Namespace: "",
|
||||||
|
Names: sets.New[string]("c1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
partitioner := rbacPartitioner{}
|
||||||
|
verb := "watch"
|
||||||
|
gotPartitions, gotErr := partitioner.All(test.apiOp, test.schema, verb, test.id)
|
||||||
|
assert.Nil(t, gotErr)
|
||||||
|
assert.Equal(t, test.wantPartitions, gotPartitions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
expectedStore := NewMockUnstructuredStore(gomock.NewController(t))
|
expectedStore := NewMockUnstructuredStore(gomock.NewController(t))
|
||||||
rp := rbacPartitioner{
|
rp := rbacPartitioner{
|
||||||
|
@ -24,8 +24,9 @@ type SchemaColumnSetter interface {
|
|||||||
|
|
||||||
// Store implements types.proxyStore for partitions.
|
// Store implements types.proxyStore for partitions.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
Partitioner Partitioner
|
Partitioner Partitioner
|
||||||
asl accesscontrol.AccessSetLookup
|
asl accesscontrol.AccessSetLookup
|
||||||
|
sqlReservedFields map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore creates a types.proxyStore implementation with a partitioner
|
// NewStore creates a types.proxyStore implementation with a partitioner
|
||||||
@ -36,6 +37,14 @@ func NewStore(store UnstructuredStore, asl accesscontrol.AccessSetLookup) *Store
|
|||||||
},
|
},
|
||||||
asl: asl,
|
asl: asl,
|
||||||
}
|
}
|
||||||
|
sqlReservedFields := map[string]bool{}
|
||||||
|
for key, value := range types.ReservedFields {
|
||||||
|
if key == "id" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sqlReservedFields[key] = value
|
||||||
|
}
|
||||||
|
s.sqlReservedFields = sqlReservedFields
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@ -48,7 +57,7 @@ func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return partition.ToAPI(schema, obj, warnings), nil
|
return partition.ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByID looks up a single object by its ID.
|
// ByID looks up a single object by its ID.
|
||||||
@ -59,7 +68,7 @@ func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return partition.ToAPI(schema, obj, warnings), nil
|
return partition.ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a list of objects across all applicable partitions.
|
// List returns a list of objects across all applicable partitions.
|
||||||
@ -85,7 +94,8 @@ func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.AP
|
|||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
item := item.DeepCopy()
|
item := item.DeepCopy()
|
||||||
result.Objects = append(result.Objects, partition.ToAPI(schema, item, nil))
|
// the sql cache automatically adds the ID through a transformFunc. Because of this, we have a different set of reserved fields for the SQL cache
|
||||||
|
result.Objects = append(result.Objects, partition.ToAPI(schema, item, nil, s.sqlReservedFields))
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Revision = ""
|
result.Revision = ""
|
||||||
@ -101,7 +111,7 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data ty
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return partition.ToAPI(schema, obj, warnings), nil
|
return partition.ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates a single object in the store.
|
// Update updates a single object in the store.
|
||||||
@ -112,7 +122,7 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data ty
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.APIObject{}, err
|
return types.APIObject{}, err
|
||||||
}
|
}
|
||||||
return partition.ToAPI(schema, obj, warnings), nil
|
return partition.ToAPI(schema, obj, warnings, types.ReservedFields), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch returns a channel of events for a list or resource.
|
// Watch returns a channel of events for a list or resource.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/rancher/steve/pkg/stores/sqlproxy (interfaces: Cache,ClientGetter,CacheFactory,SchemaColumnSetter,RelationshipNotifier)
|
// Source: github.com/rancher/steve/pkg/stores/sqlproxy (interfaces: Cache,ClientGetter,CacheFactory,SchemaColumnSetter,RelationshipNotifier,TransformBuilder)
|
||||||
|
|
||||||
// Package sqlproxy is a generated GoMock package.
|
// Package sqlproxy is a generated GoMock package.
|
||||||
package sqlproxy
|
package sqlproxy
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
dynamic "k8s.io/client-go/dynamic"
|
dynamic "k8s.io/client-go/dynamic"
|
||||||
kubernetes "k8s.io/client-go/kubernetes"
|
kubernetes "k8s.io/client-go/kubernetes"
|
||||||
rest "k8s.io/client-go/rest"
|
rest "k8s.io/client-go/rest"
|
||||||
|
cache "k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockCache is a mock of Cache interface.
|
// MockCache is a mock of Cache interface.
|
||||||
@ -257,18 +258,18 @@ func (m *MockCacheFactory) EXPECT() *MockCacheFactoryMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CacheFor mocks base method.
|
// CacheFor mocks base method.
|
||||||
func (m *MockCacheFactory) CacheFor(arg0 [][]string, arg1 dynamic.ResourceInterface, arg2 schema.GroupVersionKind, arg3 bool) (factory.Cache, error) {
|
func (m *MockCacheFactory) CacheFor(arg0 [][]string, arg1 cache.TransformFunc, arg2 dynamic.ResourceInterface, arg3 schema.GroupVersionKind, arg4, arg5 bool) (factory.Cache, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "CacheFor", arg0, arg1, arg2, arg3)
|
ret := m.ctrl.Call(m, "CacheFor", arg0, arg1, arg2, arg3, arg4, arg5)
|
||||||
ret0, _ := ret[0].(factory.Cache)
|
ret0, _ := ret[0].(factory.Cache)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheFor indicates an expected call of CacheFor.
|
// CacheFor indicates an expected call of CacheFor.
|
||||||
func (mr *MockCacheFactoryMockRecorder) CacheFor(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
func (mr *MockCacheFactoryMockRecorder) CacheFor(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CacheFor", reflect.TypeOf((*MockCacheFactory)(nil).CacheFor), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CacheFor", reflect.TypeOf((*MockCacheFactory)(nil).CacheFor), arg0, arg1, arg2, arg3, arg4, arg5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset mocks base method.
|
// Reset mocks base method.
|
||||||
@ -358,3 +359,40 @@ func (mr *MockRelationshipNotifierMockRecorder) OnInboundRelationshipChange(arg0
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnInboundRelationshipChange", reflect.TypeOf((*MockRelationshipNotifier)(nil).OnInboundRelationshipChange), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnInboundRelationshipChange", reflect.TypeOf((*MockRelationshipNotifier)(nil).OnInboundRelationshipChange), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MockTransformBuilder is a mock of TransformBuilder interface.
|
||||||
|
type MockTransformBuilder struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockTransformBuilderMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTransformBuilderMockRecorder is the mock recorder for MockTransformBuilder.
|
||||||
|
type MockTransformBuilderMockRecorder struct {
|
||||||
|
mock *MockTransformBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockTransformBuilder creates a new mock instance.
|
||||||
|
func NewMockTransformBuilder(ctrl *gomock.Controller) *MockTransformBuilder {
|
||||||
|
mock := &MockTransformBuilder{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockTransformBuilderMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockTransformBuilder) EXPECT() *MockTransformBuilderMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransformFunc mocks base method.
|
||||||
|
func (m *MockTransformBuilder) GetTransformFunc(arg0 schema.GroupVersionKind) cache.TransformFunc {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetTransformFunc", arg0)
|
||||||
|
ret0, _ := ret[0].(cache.TransformFunc)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransformFunc indicates an expected call of GetTransformFunc.
|
||||||
|
func (mr *MockTransformBuilderMockRecorder) GetTransformFunc(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransformFunc", reflect.TypeOf((*MockTransformBuilder)(nil).GetTransformFunc), arg0)
|
||||||
|
}
|
||||||
|
@ -15,20 +15,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rancher/apiserver/pkg/apierror"
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
|
||||||
"github.com/rancher/lasso/pkg/cache/sql/informer"
|
|
||||||
"github.com/rancher/lasso/pkg/cache/sql/informer/factory"
|
|
||||||
"github.com/rancher/lasso/pkg/cache/sql/partition"
|
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
|
||||||
"github.com/rancher/steve/pkg/resources/common"
|
|
||||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
|
||||||
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
|
||||||
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/data"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
@ -42,20 +28,130 @@ import (
|
|||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
|
"github.com/rancher/apiserver/pkg/apierror"
|
||||||
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
"github.com/rancher/lasso/pkg/cache/sql/informer"
|
||||||
|
"github.com/rancher/lasso/pkg/cache/sql/informer/factory"
|
||||||
|
"github.com/rancher/lasso/pkg/cache/sql/partition"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/data"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
|
|
||||||
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
|
controllerschema "github.com/rancher/steve/pkg/controllers/schema"
|
||||||
|
"github.com/rancher/steve/pkg/resources/common"
|
||||||
|
"github.com/rancher/steve/pkg/resources/virtual"
|
||||||
|
virtualCommon "github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
|
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||||
|
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
|
||||||
|
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
|
||||||
|
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
const watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
|
const (
|
||||||
|
watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
|
||||||
|
errNamespaceRequired = "metadata.namespace or apiOp.namespace are required"
|
||||||
|
errResourceVersionRequired = "metadata.resourceVersion is required for update"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
paramScheme = runtime.NewScheme()
|
paramScheme = runtime.NewScheme()
|
||||||
paramCodec = runtime.NewParameterCodec(paramScheme)
|
paramCodec = runtime.NewParameterCodec(paramScheme)
|
||||||
typeSpecificIndexedFields = map[string][][]string{
|
typeSpecificIndexedFields = map[string][][]string{
|
||||||
"_v1_Namespace": {{`metadata`, `labels[field.cattle.io/projectId]`}},
|
gvkKey("", "v1", "ConfigMap"): {
|
||||||
"_v1_Node": {{`status`, `nodeInfo`, `kubeletVersion`}, {`status`, `nodeInfo`, `operatingSystem`}},
|
{"metadata", "labels[harvesterhci.io/cloud-init-template]"}},
|
||||||
"_v1_Pod": {{`spec`, `containers`, `image`}, {`spec`, `nodeName`}},
|
gvkKey("", "v1", "Event"): {
|
||||||
"_v1_ConfigMap": {{`metadata`, `labels[harvesterhci.io/cloud-init-template]`}},
|
{"_type"},
|
||||||
|
{"involvedObject", "kind"},
|
||||||
"management.cattle.io_v3_Node": {{`status`, `nodeName`}},
|
{"message"},
|
||||||
|
{"reason"},
|
||||||
|
},
|
||||||
|
gvkKey("", "v1", "Namespace"): {
|
||||||
|
{"metadata", "labels[field.cattle.io/projectId]"}},
|
||||||
|
gvkKey("", "v1", "Node"): {
|
||||||
|
{"status", "nodeInfo", "kubeletVersion"},
|
||||||
|
{"status", "nodeInfo", "operatingSystem"}},
|
||||||
|
gvkKey("", "v1", "PersistentVolume"): {
|
||||||
|
{"status", "reason"},
|
||||||
|
{"spec", "persistentVolumeReclaimPolicy"},
|
||||||
|
},
|
||||||
|
gvkKey("", "v1", "PersistentVolumeClaim"): {
|
||||||
|
{"spec", "volumeName"}},
|
||||||
|
gvkKey("", "v1", "Pod"): {
|
||||||
|
{"spec", "containers", "image"},
|
||||||
|
{"spec", "nodeName"}},
|
||||||
|
gvkKey("", "v1", "Service"): {
|
||||||
|
{"spec", "clusterIP"},
|
||||||
|
{"spec", "type"},
|
||||||
|
},
|
||||||
|
gvkKey("apps", "v1", "DaemonSet"): {
|
||||||
|
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
||||||
|
},
|
||||||
|
gvkKey("apps", "v1", "Deployment"): {
|
||||||
|
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
||||||
|
},
|
||||||
|
gvkKey("apps", "v1", "StatefulSet"): {
|
||||||
|
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
||||||
|
},
|
||||||
|
gvkKey("autoscaling", "v2", "HorizontalPodAutoscaler"): {
|
||||||
|
{"spec", "scaleTargetRef", "name"},
|
||||||
|
{"spec", "minReplicas"},
|
||||||
|
{"spec", "maxReplicas"},
|
||||||
|
{"status", "currentReplicas"},
|
||||||
|
},
|
||||||
|
gvkKey("batch", "v1", "CronJob"): {
|
||||||
|
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
||||||
|
},
|
||||||
|
gvkKey("batch", "v1", "Job"): {
|
||||||
|
{"metadata", "annotations[field.cattle.io/publicEndpoints]"},
|
||||||
|
},
|
||||||
|
gvkKey("catalog.cattle.io", "v1", "App"): {
|
||||||
|
{"spec", "chart", "metadata", "name"},
|
||||||
|
},
|
||||||
|
gvkKey("catalog.cattle.io", "v1", "ClusterRepo"): {
|
||||||
|
{"metadata", "annotations[clusterrepo.cattle.io/hidden]"},
|
||||||
|
{"spec", "gitBranch"},
|
||||||
|
{"spec", "gitRepo"},
|
||||||
|
},
|
||||||
|
gvkKey("catalog.cattle.io", "v1", "Operation"): {
|
||||||
|
{"status", "action"},
|
||||||
|
{"status", "namespace"},
|
||||||
|
{"status", "releaseName"},
|
||||||
|
},
|
||||||
|
gvkKey("cluster.x-k8s.io", "v1beta1", "Machine"): {
|
||||||
|
{"spec", "clusterName"}},
|
||||||
|
gvkKey("management.cattle.io", "v3", "Cluster"): {
|
||||||
|
{"metadata", "labels[provider.cattle.io]"},
|
||||||
|
{"spec", "internal"},
|
||||||
|
{"spec", "displayName"},
|
||||||
|
{"status", "provider"},
|
||||||
|
},
|
||||||
|
gvkKey("management.cattle.io", "v3", "Node"): {
|
||||||
|
{"status", "nodeName"}},
|
||||||
|
gvkKey("management.cattle.io", "v3", "NodePool"): {
|
||||||
|
{"spec", "clusterName"}},
|
||||||
|
gvkKey("management.cattle.io", "v3", "NodeTemplate"): {
|
||||||
|
{"spec", "clusterName"}},
|
||||||
|
gvkKey("networking.k8s.io", "v1", "Ingress"): {
|
||||||
|
{"spec", "rules", "host"},
|
||||||
|
{"spec", "ingressClassName"},
|
||||||
|
},
|
||||||
|
gvkKey("provisioning.cattle.io", "v1", "Cluster"): {
|
||||||
|
{"metadata", "labels[provider.cattle.io]"},
|
||||||
|
{"status", "clusterName"},
|
||||||
|
{"status", "provider"},
|
||||||
|
},
|
||||||
|
gvkKey("storage.k8s.io", "v1", "StorageClass"): {
|
||||||
|
{"provisioner"},
|
||||||
|
{"metadata", "annotations[storageclass.kubernetes.io/is-default-class]"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
commonIndexFields = [][]string{
|
||||||
|
{`id`},
|
||||||
|
{`metadata`, `state`, `name`},
|
||||||
}
|
}
|
||||||
baseNSSchema = types.APISchema{
|
baseNSSchema = types.APISchema{
|
||||||
Schema: &schemas.Schema{
|
Schema: &schemas.Schema{
|
||||||
@ -118,29 +214,35 @@ type RelationshipNotifier interface {
|
|||||||
OnInboundRelationshipChange(ctx context.Context, schema *types.APISchema, namespace string) <-chan *summary.Relationship
|
OnInboundRelationshipChange(ctx context.Context, schema *types.APISchema, namespace string) <-chan *summary.Relationship
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransformBuilder interface {
|
||||||
|
GetTransformFunc(gvk schema.GroupVersionKind) cache.TransformFunc
|
||||||
|
}
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
clientGetter ClientGetter
|
clientGetter ClientGetter
|
||||||
notifier RelationshipNotifier
|
notifier RelationshipNotifier
|
||||||
cacheFactory CacheFactory
|
cacheFactory CacheFactory
|
||||||
cfInitializer CacheFactoryInitializer
|
cfInitializer CacheFactoryInitializer
|
||||||
namespaceCache Cache
|
namespaceCache Cache
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
columnSetter SchemaColumnSetter
|
columnSetter SchemaColumnSetter
|
||||||
|
transformBuilder TransformBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheFactoryInitializer func() (CacheFactory, error)
|
type CacheFactoryInitializer func() (CacheFactory, error)
|
||||||
|
|
||||||
type CacheFactory interface {
|
type CacheFactory interface {
|
||||||
CacheFor(fields [][]string, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool) (factory.Cache, error)
|
CacheFor(fields [][]string, transform cache.TransformFunc, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool, watchable bool) (factory.Cache, error)
|
||||||
Reset() error
|
Reset() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProxyStore returns a Store implemented directly on top of kubernetes.
|
// NewProxyStore returns a Store implemented directly on top of kubernetes.
|
||||||
func NewProxyStore(c SchemaColumnSetter, clientGetter ClientGetter, notifier RelationshipNotifier, factory CacheFactory) (*Store, error) {
|
func NewProxyStore(c SchemaColumnSetter, clientGetter ClientGetter, notifier RelationshipNotifier, scache virtualCommon.SummaryCache, factory CacheFactory) (*Store, error) {
|
||||||
store := &Store{
|
store := &Store{
|
||||||
clientGetter: clientGetter,
|
clientGetter: clientGetter,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
columnSetter: c,
|
columnSetter: c,
|
||||||
|
transformBuilder: virtual.NewTransformBuilder(scache),
|
||||||
}
|
}
|
||||||
|
|
||||||
if factory == nil {
|
if factory == nil {
|
||||||
@ -197,14 +299,18 @@ func (s *Store) initializeNamespaceCache() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gvk := attributes.GVK(&nsSchema)
|
||||||
// get fields from schema's columns
|
// get fields from schema's columns
|
||||||
fields := getFieldsFromSchema(&nsSchema)
|
fields := getFieldsFromSchema(&nsSchema)
|
||||||
|
|
||||||
// get any type-specific fields that steve is interested in
|
// get any type-specific fields that steve is interested in
|
||||||
fields = append(fields, getFieldForGVK(attributes.GVK(&nsSchema))...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
|
|
||||||
|
// get the type-specifc transform func
|
||||||
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
||||||
|
|
||||||
// get the ns informer
|
// get the ns informer
|
||||||
nsInformer, err := s.cacheFactory.CacheFor(fields, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(&nsSchema), false)
|
nsInformer, err := s.cacheFactory.CacheFor(fields, transformFunc, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(&nsSchema), false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -214,11 +320,17 @@ func (s *Store) initializeNamespaceCache() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFieldForGVK(gvk schema.GroupVersionKind) [][]string {
|
func getFieldForGVK(gvk schema.GroupVersionKind) [][]string {
|
||||||
return typeSpecificIndexedFields[keyFromGVK(gvk)]
|
fields := [][]string{}
|
||||||
|
fields = append(fields, commonIndexFields...)
|
||||||
|
typeFields := typeSpecificIndexedFields[gvkKey(gvk.Group, gvk.Version, gvk.Kind)]
|
||||||
|
if typeFields != nil {
|
||||||
|
fields = append(fields, typeFields...)
|
||||||
|
}
|
||||||
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyFromGVK(gvk schema.GroupVersionKind) string {
|
func gvkKey(group, version, kind string) string {
|
||||||
return gvk.Group + "_" + gvk.Version + "_" + gvk.Kind
|
return group + "_" + version + "_" + kind
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFieldsFromSchema converts object field names from types.APISchema's format into lasso's
|
// getFieldsFromSchema converts object field names from types.APISchema's format into lasso's
|
||||||
@ -483,20 +595,27 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params
|
|||||||
}
|
}
|
||||||
|
|
||||||
name := types.Name(input)
|
name := types.Name(input)
|
||||||
ns := types.Namespace(input)
|
namespace := types.Namespace(input)
|
||||||
if name == "" && input.String("metadata", "generateName") == "" {
|
generateName := input.String("metadata", "generateName")
|
||||||
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
|
|
||||||
|
if name == "" && generateName == "" {
|
||||||
|
input.SetNested(schema.ID[0:1]+"-", "metadata", "generateName")
|
||||||
}
|
}
|
||||||
if ns == "" && apiOp.Namespace != "" {
|
|
||||||
ns = apiOp.Namespace
|
if attributes.Namespaced(schema) && namespace == "" {
|
||||||
input.SetNested(ns, "metadata", "namespace")
|
if apiOp.Namespace == "" {
|
||||||
|
return nil, nil, apierror.NewAPIError(validation.InvalidBodyContent, errNamespaceRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace = apiOp.Namespace
|
||||||
|
input.SetNested(namespace, "metadata", "namespace")
|
||||||
}
|
}
|
||||||
|
|
||||||
gvk := attributes.GVK(schema)
|
gvk := attributes.GVK(schema)
|
||||||
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||||
|
|
||||||
buffer := WarningBuffer{}
|
buffer := WarningBuffer{}
|
||||||
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer))
|
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, namespace, &buffer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -563,9 +682,12 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params
|
|||||||
|
|
||||||
resourceVersion := input.String("metadata", "resourceVersion")
|
resourceVersion := input.String("metadata", "resourceVersion")
|
||||||
if resourceVersion == "" {
|
if resourceVersion == "" {
|
||||||
return nil, nil, fmt.Errorf("metadata.resourceVersion is required for update")
|
return nil, nil, errors.New(errResourceVersionRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gvk := attributes.GVK(schema)
|
||||||
|
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||||
|
|
||||||
opts := metav1.UpdateOptions{}
|
opts := metav1.UpdateOptions{}
|
||||||
if err := decodeParams(apiOp, &opts); err != nil {
|
if err := decodeParams(apiOp, &opts); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -623,10 +745,12 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, schema *types.APISchem
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, "", err
|
return nil, 0, "", err
|
||||||
}
|
}
|
||||||
|
gvk := attributes.GVK(schema)
|
||||||
fields := getFieldsFromSchema(schema)
|
fields := getFieldsFromSchema(schema)
|
||||||
fields = append(fields, getFieldForGVK(attributes.GVK(schema))...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
||||||
|
|
||||||
inf, err := s.cacheFactory.CacheFor(fields, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(schema), attributes.Namespaced(schema))
|
inf, err := s.cacheFactory.CacheFor(fields, transformFunc, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(schema), attributes.Namespaced(schema), controllerschema.IsListWatchable(schema))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, "", err
|
return nil, 0, "", err
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user