1
0
mirror of https://github.com/rancher/steve.git synced 2025-04-27 02:51:10 +00:00

SQLite backed cache (#223)

This uses SQLite-backed informers provided by Lasso with https://github.com/rancher/lasso/pull/65 to implement Steve API (/v1/) functionality.

This new functionality is available behind a feature flag to be specified at Steve startup

See https://confluence.suse.com/pages/viewpage.action?pageId=1359086083 

Co-authored-by: Ricardo Weir <ricardo.weir@suse.com>
Co-authored-by: Michael Bolot <michael.bolot@suse.com>
Co-authored-by: Silvio Moioli <silvio@moioli.net>
Signed-off-by: Silvio Moioli <silvio@moioli.net>
This commit is contained in:
Silvio Moioli 2024-06-05 16:17:12 +02:00 committed by GitHub
parent 4cf4e6b385
commit 7a84620e8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 4826 additions and 46 deletions

26
go.mod
View File

@ -1,6 +1,8 @@
module github.com/rancher/steve
go 1.22
go 1.22.0
toolchain go1.22.2
replace (
github.com/crewjam/saml => github.com/rancher/saml v0.2.0
@ -21,6 +23,7 @@ require (
github.com/rancher/apiserver v0.0.0-20240503193545-2e1b0ddd9791
github.com/rancher/dynamiclistener v0.5.0-rc6
github.com/rancher/kubernetes-provider-detector v0.1.5
github.com/rancher/lasso v0.0.0-20240603075835-701e919d08b7
github.com/rancher/norman v0.0.0-20240503193601-9f5f6586bb5b
github.com/rancher/remotedialer v0.3.2
github.com/rancher/wrangler/v2 v2.2.0-rc6
@ -30,14 +33,14 @@ require (
github.com/urfave/cli/v2 v2.27.1
golang.org/x/sync v0.7.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.29.3
k8s.io/api v0.30.0
k8s.io/apiextensions-apiserver v0.29.3
k8s.io/apimachinery v0.29.3
k8s.io/apimachinery v0.30.0
k8s.io/apiserver v0.29.3
k8s.io/client-go v12.0.0+incompatible
k8s.io/klog v1.0.0
k8s.io/kube-aggregator v0.29.3
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
)
require (
@ -47,6 +50,7 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
@ -60,22 +64,25 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.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/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rancher/lasso v0.0.0-20240424194130-d87ec407d941 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
@ -104,6 +111,13 @@ require (
k8s.io/component-base v0.29.3 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.29.10 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect
sigs.k8s.io/cli-utils v0.35.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect

63
go.sum
View File

@ -659,6 +659,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
@ -823,8 +825,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
@ -832,8 +835,9 @@ github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
@ -871,6 +875,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -914,6 +920,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@ -932,6 +940,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -954,8 +964,9 @@ github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxe
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@ -975,8 +986,9 @@ github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+q
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE=
github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@ -1011,8 +1023,8 @@ github.com/rancher/dynamiclistener v0.5.0-rc6 h1:F/WVZ/asQAHIVxBq7Q448ORfnKti4vh
github.com/rancher/dynamiclistener v0.5.0-rc6/go.mod h1:dDgzEszqQTOnXq2vulbHVXeitbngEnNU0bZoV8qAIYw=
github.com/rancher/kubernetes-provider-detector v0.1.5 h1:hWRAsWuJOemzGjz/XrbTlM7QmfO4OedvFE3QwXiH60I=
github.com/rancher/kubernetes-provider-detector v0.1.5/go.mod h1:ypuJS7kP7rUiAn330xG46mj+Nhvym05GM8NqMVekpH0=
github.com/rancher/lasso v0.0.0-20240424194130-d87ec407d941 h1:1SvuoeyfANRvKVJUSzHWa1P781iuH8ktUjW9cPOxAAk=
github.com/rancher/lasso v0.0.0-20240424194130-d87ec407d941/go.mod h1:pYKOe2r/5O0w3ypoc7xHQF8LvWCp5PsNRea1Jpq3vBU=
github.com/rancher/lasso v0.0.0-20240603075835-701e919d08b7 h1:E5AeOkylBXf4APhnHgDvePdtpxOfIjhKnxfjm4sDIEk=
github.com/rancher/lasso v0.0.0-20240603075835-701e919d08b7/go.mod h1:v0FJLrmL4m6zdWfIB0/qo7qN5QIjVMFyvFGaw8uyWsA=
github.com/rancher/norman v0.0.0-20240503193601-9f5f6586bb5b h1:9k8VOhRi6ZIZ8rBlQG8ON9eG+ukqThNeXJ2e6CzZO78=
github.com/rancher/norman v0.0.0-20240503193601-9f5f6586bb5b/go.mod h1:xJ0CLJUG9SvtyuPzPA8ATh2SjwiqXGfE+pPh7uVhJzQ=
github.com/rancher/remotedialer v0.3.2 h1:kstZbRwPS5gPWpGg8VjEHT2poHtArs+Fc317YM8JCzU=
@ -1020,6 +1032,8 @@ github.com/rancher/remotedialer v0.3.2/go.mod h1:Ys004RpJuTLSm+k4aYUCoFiOOad37ub
github.com/rancher/wrangler/v2 v2.2.0-rc6 h1:jMsuOVl7nBuQ5QJqdNkR2yHEf1+rYiyd1gN+mQzIcag=
github.com/rancher/wrangler/v2 v2.2.0-rc6/go.mod h1:rFxhBR+PpC1MuJli+JeMpxoGxfV7XdFWtpdLC8s+oWQ=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -1182,6 +1196,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1859,13 +1875,15 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI=
k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc=
k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/apiserver v0.29.3 h1:xR7ELlJ/BZSr2n4CnD3lfA4gzFivh0wwfNfz9L0WZcE=
k8s.io/apiserver v0.29.3/go.mod h1:hrvXlwfRulbMbBgmWRQlFru2b/JySDpmzvQwwk4GUOs=
k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo=
@ -1885,8 +1903,8 @@ k8s.io/kube-aggregator v0.29.3 h1:5KvTyFN8sQq2imq8tMAHWEKoE64Zg9WSMaGX78KV6ps=
k8s.io/kube-aggregator v0.29.3/go.mod h1:xGJqV/SJJ1fbwTGfQLAZfwgqX1EMoaqfotDTkDrqqSk=
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA=
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
@ -1895,13 +1913,23 @@ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
@ -1910,19 +1938,34 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -34,7 +34,7 @@ func main() {
func run(_ *cli.Context) error {
ctx := signals.SetupSignalContext()
debugconfig.MustSetupDebug()
s, err := config.ToServer(ctx)
s, err := config.ToServer(ctx, false)
if err != nil {
return err
}

View File

@ -31,8 +31,10 @@ var (
}
)
type SchemasHandler interface {
OnSchemas(schemas *schema2.Collection) error
type SchemasHandlerFunc func(schemas *schema2.Collection) error
func (s SchemasHandlerFunc) OnSchemas(schemas *schema2.Collection) error {
return s(schemas)
}
type handler struct {
@ -45,7 +47,7 @@ type handler struct {
cols *common.DynamicColumns
crd apiextcontrollerv1.CustomResourceDefinitionClient
ssar authorizationv1client.SelfSubjectAccessReviewInterface
handler SchemasHandler
handler SchemasHandlerFunc
}
func Register(ctx context.Context,
@ -54,7 +56,7 @@ func Register(ctx context.Context,
crd apiextcontrollerv1.CustomResourceDefinitionController,
apiService v1.APIServiceController,
ssar authorizationv1client.SelfSubjectAccessReviewInterface,
schemasHandler SchemasHandler,
schemasHandler SchemasHandlerFunc,
schemas *schema2.Collection) {
h := &handler{

View File

@ -30,6 +30,14 @@ func DefaultTemplate(clientGetter proxy.ClientGetter,
}
}
// DefaultTemplateForStore provides a default schema template which uses a provided, pre-initialized store. Primarily used when creating a Template that uses a Lasso SQL store internally.
func DefaultTemplateForStore(store types.Store, summaryCache *summarycache.SummaryCache) schema.Template {
return schema.Template{
Store: store,
Formatter: formatter(summaryCache),
}
}
func selfLink(gvr schema2.GroupVersionResource, meta metav1.Object) (prefix string) {
buf := &strings.Builder{}
if gvr.Group == "management.cattle.io" && gvr.Version == "v3" {

View File

@ -71,3 +71,33 @@ func DefaultSchemaTemplates(cf *client.Factory,
},
}
}
// DefaultSchemaTemplatesForStore returns the same default templates as DefaultSchemaTemplates, only using DefaultSchemaTemplateFoStore internally to construct the templates.
func DefaultSchemaTemplatesForStore(store types.Store,
baseSchemas *types.APISchemas,
summaryCache *summarycache.SummaryCache,
discovery discovery.DiscoveryInterface) []schema.Template {
return []schema.Template{
common.DefaultTemplateForStore(store, summaryCache),
apigroups.Template(discovery),
{
ID: "configmap",
Formatter: formatters.DropHelmData,
},
{
ID: "secret",
Formatter: formatters.DropHelmData,
},
{
ID: "pod",
Formatter: formatters.Pod,
},
{
ID: "management.cattle.io.cluster",
Customize: func(apiSchema *types.APISchema) {
cluster.AddApply(baseSchemas, apiSchema)
},
},
}
}

View File

@ -23,14 +23,14 @@ type Config struct {
}
func (c *Config) MustServer(ctx context.Context) *server.Server {
cc, err := c.ToServer(ctx)
cc, err := c.ToServer(ctx, false)
if err != nil {
panic(err)
}
return cc
}
func (c *Config) ToServer(ctx context.Context) (*server.Server, error) {
func (c *Config) ToServer(ctx context.Context, sqlCache bool) (*server.Server, error) {
var (
auth steveauth.Middleware
)
@ -51,6 +51,7 @@ func (c *Config) ToServer(ctx context.Context) (*server.Server, error) {
return server.New(ctx, restConfig, &server.Options{
AuthMiddleware: auth,
Next: ui.New(c.UIPath),
SQLCache: sqlCache,
})
}

View File

@ -21,6 +21,10 @@ import (
"github.com/rancher/steve/pkg/schema/definitions"
"github.com/rancher/steve/pkg/server/handler"
"github.com/rancher/steve/pkg/server/router"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/proxy"
"github.com/rancher/steve/pkg/stores/sqlpartition"
"github.com/rancher/steve/pkg/stores/sqlproxy"
"github.com/rancher/steve/pkg/summarycache"
"k8s.io/client-go/rest"
)
@ -48,6 +52,7 @@ type Server struct {
aggregationSecretNamespace string
aggregationSecretName string
SQLCache bool
}
type Options struct {
@ -62,6 +67,8 @@ type Options struct {
AggregationSecretName string
ClusterRegistry string
ServerVersion string
// SQLCache enables the SQLite-based lasso caching mechanism
SQLCache bool
}
func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server, error) {
@ -81,6 +88,8 @@ func New(ctx context.Context, restConfig *rest.Config, opts *Options) (*Server,
aggregationSecretName: opts.AggregationSecretName,
ClusterRegistry: opts.ClusterRegistry,
Version: opts.ServerVersion,
// SQLCache enables the SQLite-based lasso caching mechanism
SQLCache: opts.SQLCache,
}
if err := setup(ctx, server); err != nil {
@ -147,16 +156,52 @@ func setup(ctx context.Context, server *Server) error {
summaryCache := summarycache.New(sf, ccache)
summaryCache.Start(ctx)
for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), server.controllers.Core.Namespace().Cache()) {
sf.AddTemplate(template)
}
cols, err := common.NewDynamicColumns(server.RESTConfig)
if err != nil {
return err
}
var onSchemasHandler schemacontroller.SchemasHandlerFunc
if server.SQLCache {
s, err := sqlproxy.NewProxyStore(cols, cf, summaryCache, nil)
if err != nil {
panic(err)
}
errStore := proxy.NewErrorStore(
proxy.NewUnformatterStore(
proxy.NewWatchRefresh(
sqlpartition.NewStore(
s,
asl,
),
asl,
),
),
)
store := metricsStore.NewMetricsStore(errStore)
// end store setup code
for _, template := range resources.DefaultSchemaTemplatesForStore(store, server.BaseSchemas, summaryCache, server.controllers.K8s.Discovery()) {
sf.AddTemplate(template)
}
onSchemasHandler = func(schemas *schema.Collection) error {
if err := ccache.OnSchemas(schemas); err != nil {
return err
}
if err := s.Reset(); err != nil {
return err
}
return nil
}
} else {
for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), server.controllers.Core.Namespace().Cache()) {
sf.AddTemplate(template)
}
onSchemasHandler = ccache.OnSchemas
}
schemas.SetupWatcher(ctx, server.BaseSchemas, asl, sf)
schemacontroller.Register(ctx,
@ -165,7 +210,7 @@ func setup(ctx context.Context, server *Server) error {
server.controllers.CRD.CustomResourceDefinition(),
server.controllers.API.APIService(),
server.controllers.K8s.AuthorizationV1().SelfSubjectAccessReviews(),
ccache,
onSchemasHandler,
sf)
apiServer, handler, err := handler.New(server.RESTConfig, sf, server.authMiddleware, server.next, server.router)
@ -176,6 +221,7 @@ func setup(ctx context.Context, server *Server) error {
server.APIServer = apiServer
server.Handler = handler
server.SchemaFactory = sf
return nil
}

View File

@ -110,7 +110,7 @@ func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id stri
if err != nil {
return types.APIObject{}, err
}
return toAPI(schema, obj, warnings), nil
return ToAPI(schema, obj, warnings), nil
}
// ByID looks up a single object by its ID.
@ -124,7 +124,7 @@ func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string
if err != nil {
return types.APIObject{}, err
}
return toAPI(schema, obj, warnings), nil
return ToAPI(schema, obj, warnings), nil
}
func (s *Store) listPartition(ctx context.Context, apiOp *types.APIRequest, schema *types.APISchema, partition Partition,
@ -226,7 +226,7 @@ func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.AP
for _, item := range list {
item := item.DeepCopy()
result.Objects = append(result.Objects, toAPI(schema, item, nil))
result.Objects = append(result.Objects, ToAPI(schema, item, nil))
}
result.Pages = pages
@ -266,7 +266,7 @@ func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data ty
if err != nil {
return types.APIObject{}, err
}
return toAPI(schema, obj, warnings), nil
return ToAPI(schema, obj, warnings), nil
}
// Update updates a single object in the store.
@ -280,7 +280,7 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data ty
if err != nil {
return types.APIObject{}, err
}
return toAPI(schema, obj, warnings), nil
return ToAPI(schema, obj, warnings), nil
}
// Watch returns a channel of events for a list or resource.
@ -310,7 +310,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types
return err
}
for i := range c {
response <- toAPIEvent(apiOp, schema, i)
response <- ToAPIEvent(apiOp, schema, i)
}
return nil
})
@ -326,7 +326,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types
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) types.APIObject {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return types.APIObject{}
}
@ -372,7 +372,7 @@ func moveToUnderscore(obj *unstructured.Unstructured) *unstructured.Unstructured
return obj
}
func toAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, event watch.Event) types.APIEvent {
func ToAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, event watch.Event) types.APIEvent {
name := types.ChangeAPIEvent
switch event.Type {
case watch.Deleted:
@ -393,7 +393,7 @@ func toAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, event watch.Ev
return apiEvent
}
apiEvent.Object = toAPI(schema, event.Object, nil)
apiEvent.Object = ToAPI(schema, event.Object, nil)
m, err := meta.Accessor(event.Object)
if err != nil {

View File

@ -7,43 +7,49 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
)
type errorStore struct {
// ErrorStore implements types.store with errors translated into APIErrors
type ErrorStore struct {
types.Store
}
// NewErrorStore returns a store with errors translated into APIErrors
func NewErrorStore(s types.Store) *ErrorStore {
return &ErrorStore{Store: s}
}
// ByID looks up a single object by its ID.
func (e *errorStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
func (e *ErrorStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
data, err := e.Store.ByID(apiOp, schema, id)
return data, translateError(err)
}
// List returns a list of resources.
func (e *errorStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
func (e *ErrorStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
data, err := e.Store.List(apiOp, schema)
return data, translateError(err)
}
// Create creates a single object in the store.
func (e *errorStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
func (e *ErrorStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
data, err := e.Store.Create(apiOp, schema, data)
return data, translateError(err)
}
// Update updates a single object in the store.
func (e *errorStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
func (e *ErrorStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
data, err := e.Store.Update(apiOp, schema, data, id)
return data, translateError(err)
}
// Delete deletes an object from a store.
func (e *errorStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
func (e *ErrorStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
data, err := e.Store.Delete(apiOp, schema, id)
return data, translateError(err)
}
// Watch returns a channel of events for a list or resource.
func (e *errorStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
func (e *ErrorStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
data, err := e.Store.Watch(apiOp, schema, wr)
return data, translateError(err)
}

View File

@ -87,7 +87,7 @@ type Store struct {
// NewProxyStore returns a wrapped types.Store.
func NewProxyStore(clientGetter ClientGetter, notifier RelationshipNotifier, lookup accesscontrol.AccessSetLookup, namespaceCache corecontrollers.NamespaceCache) types.Store {
return &errorStore{
return &ErrorStore{
Store: &unformatterStore{
Store: &WatchRefresh{
Store: partition.NewStore(

View File

@ -11,6 +11,11 @@ type unformatterStore struct {
types.Store
}
// NewUnformatterStore returns a store which removes fields added by the formatter that kubernetes cannot recognize.
func NewUnformatterStore(s types.Store) types.Store {
return &unformatterStore{Store: s}
}
// ByID looks up a single object by its ID.
func (u *unformatterStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
return u.Store.ByID(apiOp, schema, id)

View File

@ -15,6 +15,14 @@ type WatchRefresh struct {
asl accesscontrol.AccessSetLookup
}
// NewWatchRefresh returns a new store with awareness of changes to the requester's access.
func NewWatchRefresh(s types.Store, asl accesscontrol.AccessSetLookup) *WatchRefresh {
return &WatchRefresh{
Store: s,
asl: asl,
}
}
// Watch performs a watch request which halts if the user's access level changes.
func (w *WatchRefresh) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
user, ok := request.UserFrom(apiOp.Context())

View File

@ -0,0 +1,196 @@
// Package listprocessor contains methods for filtering, sorting, and paginating lists of objects.
package listprocessor
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"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/partition"
"github.com/rancher/wrangler/v2/pkg/schemas/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
defaultLimit = 100000
continueParam = "continue"
limitParam = "limit"
filterParam = "filter"
sortParam = "sort"
pageSizeParam = "pagesize"
pageParam = "page"
revisionParam = "revision"
projectsOrNamespacesVar = "projectsornamespaces"
projectIDFieldLabel = "field.cattle.io/projectId"
orOp = ","
notOp = "!"
)
var opReg = regexp.MustCompile(`[!]?=`)
// ListOptions represents the query parameters that may be included in a list request.
type ListOptions struct {
ChunkSize int
Resume string
Filters []informer.OrFilter
Sort informer.Sort
Pagination informer.Pagination
}
type Cache interface {
// ListByOptions returns objects according to the specified list options and partitions
ListByOptions(ctx context.Context, lo informer.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, string, error)
}
// ParseQuery parses the query params of a request and returns a ListOptions.
func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (informer.ListOptions, error) {
opts := informer.ListOptions{}
opts.ChunkSize = getLimit(apiOp)
q := apiOp.Request.URL.Query()
cont := q.Get(continueParam)
opts.Resume = cont
filterParams := q[filterParam]
filterOpts := []informer.OrFilter{}
for _, filters := range filterParams {
orFilters := strings.Split(filters, orOp)
orFilter := informer.OrFilter{}
for _, filter := range orFilters {
var op informer.Op
if strings.Contains(filter, "!=") {
op = "!="
}
filter := opReg.Split(filter, -1)
if len(filter) != 2 {
continue
}
usePartialMatch := !(strings.HasPrefix(filter[1], `'`) && strings.HasSuffix(filter[1], `'`))
value := strings.TrimSuffix(strings.TrimPrefix(filter[1], "'"), "'")
orFilter.Filters = append(orFilter.Filters, informer.Filter{Field: strings.Split(filter[0], "."), Match: value, Op: op, Partial: usePartialMatch})
}
filterOpts = append(filterOpts, orFilter)
}
opts.Filters = filterOpts
sortOpts := informer.Sort{}
sortKeys := q.Get(sortParam)
if sortKeys != "" {
sortParts := strings.SplitN(sortKeys, ",", 2)
primaryField := sortParts[0]
if primaryField != "" && primaryField[0] == '-' {
sortOpts.PrimaryOrder = informer.DESC
primaryField = primaryField[1:]
}
if primaryField != "" {
sortOpts.PrimaryField = strings.Split(primaryField, ".")
}
if len(sortParts) > 1 {
secondaryField := sortParts[1]
if secondaryField != "" && secondaryField[0] == '-' {
sortOpts.SecondaryOrder = informer.DESC
secondaryField = secondaryField[1:]
}
if secondaryField != "" {
sortOpts.SecondaryField = strings.Split(secondaryField, ".")
}
}
}
opts.Sort = sortOpts
var err error
pagination := informer.Pagination{}
pagination.PageSize, err = strconv.Atoi(q.Get(pageSizeParam))
if err != nil {
pagination.PageSize = 0
}
pagination.Page, err = strconv.Atoi(q.Get(pageParam))
if err != nil {
pagination.Page = 1
}
opts.Pagination = pagination
var op informer.Op
projectsOrNamespaces := q.Get(projectsOrNamespacesVar)
if projectsOrNamespaces == "" {
projectsOrNamespaces = q.Get(projectsOrNamespacesVar + notOp)
if projectsOrNamespaces != "" {
op = informer.NotEq
}
}
if projectsOrNamespaces != "" {
projOrNSFilters, err := parseNamespaceOrProjectFilters(apiOp.Context(), projectsOrNamespaces, op, namespaceCache)
if err != nil {
return opts, err
}
if projOrNSFilters == nil {
return opts, apierror.NewAPIError(validation.NotFound, fmt.Sprintf("could not find any namespacess named [%s] or namespaces belonging to project named [%s]", projectsOrNamespaces, projectsOrNamespaces))
}
if op == informer.NotEq {
for _, filter := range projOrNSFilters {
opts.Filters = append(opts.Filters, informer.OrFilter{Filters: []informer.Filter{filter}})
}
} else {
opts.Filters = append(opts.Filters, informer.OrFilter{Filters: projOrNSFilters})
}
}
return opts, nil
}
// getLimit extracts the limit parameter from the request or sets a default of 100000.
// The default limit can be explicitly disabled by setting it to zero or negative.
// If the default is accepted, clients must be aware that the list may be incomplete, and use the "continue" token to get the next chunk of results.
func getLimit(apiOp *types.APIRequest) int {
limitString := apiOp.Request.URL.Query().Get(limitParam)
limit, err := strconv.Atoi(limitString)
if err != nil {
limit = defaultLimit
}
return limit
}
func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op informer.Op, namespaceInformer Cache) ([]informer.Filter, error) {
var filters []informer.Filter
for _, pn := range strings.Split(projOrNS, ",") {
uList, _, err := namespaceInformer.ListByOptions(ctx, informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: pn,
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: pn,
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "")
if err != nil {
return filters, err
}
for _, item := range uList.Items {
filters = append(filters, informer.Filter{
Field: []string{"metadata", "namespace"},
Match: item.GetName(),
Op: op,
Partial: false,
})
}
continue
}
return filters, nil
}

View File

@ -0,0 +1,524 @@
package listprocessor
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/informer"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
//go:generate mockgen --build_flags=--mod=mod -package listprocessor -destination ./proxy_mocks_test.go github.com/rancher/steve/pkg/stores/sqlproxy Cache
func TestParseQuery(t *testing.T) {
type testCase struct {
description string
setupNSCache func() Cache
nsc Cache
req *types.APIRequest
expectedLO informer.ListOptions
errExpected bool
}
var tests []testCase
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. Should have proper defaults set.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: ""},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" +
" and nsc returns namespaces, they should be included as filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
list := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "ns1",
},
},
},
},
}
nsc := NewMockCache(gomock.NewController(t))
nsc.EXPECT().ListByOptions(context.Background(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, "", nil)
return nsc
},
})
tests = append(tests, testCase{
description: "ParseQuery() with a namespace informer error returned should return an error.",
req: &types.APIRequest{
Request: &http.Request{
// namespace informer is only used if projectsornamespace param is given
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(nil, "", fmt.Errorf("error"))
return nsi
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" +
" and nsc does not return namespaces, an error should be returned.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "namespace"},
Match: "ns1",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
errExpected: true,
setupNSCache: func() Cache {
list := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{},
}
nsi := NewMockCache(gomock.NewController(t))
nsi.EXPECT().ListByOptions(context.Background(), informer.ListOptions{
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"metadata", "name"},
Match: "somethin",
Op: informer.Eq,
},
{
Field: []string{"metadata", "labels[field.cattle.io/projectId]"},
Match: "somethin",
Op: informer.Eq,
},
},
},
},
}, []partition.Partition{{Passthrough: true}}, "").Return(list, "", nil)
return nsi
},
})
tests = append(tests, testCase{
description: "ParseQuery() with filter param set should include filter with partial set to true in list options.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with filter param set, with value in single quotes, should include filter with partial set to false in list options.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a='c'"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: false,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with multiple filter params, should include multiple or filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c&filter=b=d"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
},
},
{
Filters: []informer.Filter{
{
Field: []string{"b"},
Match: "d",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with a filter param with a comma separate value, should include a single or filter with" +
" multiple filters.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "filter=a=c,b=d"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: []informer.OrFilter{
{
Filters: []informer.Filter{
{
Field: []string{"a"},
Match: "c",
Op: "",
Partial: true,
},
{
Field: []string{"b"},
Match: "d",
Op: "",
Partial: true,
},
},
},
},
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If one sort param is given, primary field" +
" sort option should be set",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=metadata.name"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If one sort param is given primary field " +
"and hyphen prefix for field value, sort option should be set with descending order.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=-metadata.name"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
PrimaryOrder: informer.DESC,
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If two sort params are given, sort " +
"options with primary field and secondary field should be set.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "sort=-metadata.name,spec.something"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Sort: informer.Sort{
PrimaryField: []string{"metadata", "name"},
PrimaryOrder: informer.DESC,
SecondaryField: []string{"spec", "something"},
SecondaryOrder: informer.ASC,
},
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If continue params is given, resume" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "continue=5"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Resume: "5",
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If continue param is given, resume" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "continue=5"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Resume: "5",
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If limit param is given, chunksize" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "limit=3"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: 3,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If page param is given, page" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "page=3"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
Page: 3,
},
},
setupNSCache: func() Cache {
return nil
},
})
tests = append(tests, testCase{
description: "ParseQuery() with no errors returned should returned no errors. If pagesize param is given, pageSize" +
" should be set with assigned value.",
req: &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "pagesize=20"},
},
},
expectedLO: informer.ListOptions{
ChunkSize: defaultLimit,
Filters: make([]informer.OrFilter, 0),
Pagination: informer.Pagination{
PageSize: 20,
Page: 1,
},
},
setupNSCache: func() Cache {
return nil
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
test.nsc = test.setupNSCache()
lo, err := ParseQuery(test.req, test.nsc)
if test.errExpected {
assert.NotNil(t, err)
return
}
assert.Equal(t, test.expectedLO, lo)
})
}
}

View File

@ -0,0 +1,54 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/steve/pkg/stores/sqlproxy (interfaces: Cache)
// Package listprocessor is a generated GoMock package.
package listprocessor
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
informer "github.com/rancher/lasso/pkg/cache/sql/informer"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MockCache is a mock of Cache interface.
type MockCache struct {
ctrl *gomock.Controller
recorder *MockCacheMockRecorder
}
// MockCacheMockRecorder is the mock recorder for MockCache.
type MockCacheMockRecorder struct {
mock *MockCache
}
// NewMockCache creates a new mock instance.
func NewMockCache(ctrl *gomock.Controller) *MockCache {
mock := &MockCache{ctrl: ctrl}
mock.recorder = &MockCacheMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCache) EXPECT() *MockCacheMockRecorder {
return m.recorder
}
// ListByOptions mocks base method.
func (m *MockCache) ListByOptions(arg0 context.Context, arg1 informer.ListOptions, arg2 []partition.Partition, arg3 string) (*unstructured.UnstructuredList, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByOptions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByOptions indicates an expected call of ListByOptions.
func (mr *MockCacheMockRecorder) ListByOptions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOptions", reflect.TypeOf((*MockCache)(nil).ListByOptions), arg0, arg1, arg2, arg3)
}

View File

@ -0,0 +1,185 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/steve/pkg/stores/sqlpartition (interfaces: Partitioner,UnstructuredStore)
// Package sqlpartition is a generated GoMock package.
package sqlpartition
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
types "github.com/rancher/apiserver/pkg/types"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
watch "k8s.io/apimachinery/pkg/watch"
)
// MockPartitioner is a mock of Partitioner interface.
type MockPartitioner struct {
ctrl *gomock.Controller
recorder *MockPartitionerMockRecorder
}
// MockPartitionerMockRecorder is the mock recorder for MockPartitioner.
type MockPartitionerMockRecorder struct {
mock *MockPartitioner
}
// NewMockPartitioner creates a new mock instance.
func NewMockPartitioner(ctrl *gomock.Controller) *MockPartitioner {
mock := &MockPartitioner{ctrl: ctrl}
mock.recorder = &MockPartitionerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPartitioner) EXPECT() *MockPartitionerMockRecorder {
return m.recorder
}
// All mocks base method.
func (m *MockPartitioner) All(arg0 *types.APIRequest, arg1 *types.APISchema, arg2, arg3 string) ([]partition.Partition, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "All", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].([]partition.Partition)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// All indicates an expected call of All.
func (mr *MockPartitionerMockRecorder) All(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockPartitioner)(nil).All), arg0, arg1, arg2, arg3)
}
// Store mocks base method.
func (m *MockPartitioner) Store() UnstructuredStore {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Store")
ret0, _ := ret[0].(UnstructuredStore)
return ret0
}
// Store indicates an expected call of Store.
func (mr *MockPartitionerMockRecorder) Store() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Store", reflect.TypeOf((*MockPartitioner)(nil).Store))
}
// MockUnstructuredStore is a mock of UnstructuredStore interface.
type MockUnstructuredStore struct {
ctrl *gomock.Controller
recorder *MockUnstructuredStoreMockRecorder
}
// MockUnstructuredStoreMockRecorder is the mock recorder for MockUnstructuredStore.
type MockUnstructuredStoreMockRecorder struct {
mock *MockUnstructuredStore
}
// NewMockUnstructuredStore creates a new mock instance.
func NewMockUnstructuredStore(ctrl *gomock.Controller) *MockUnstructuredStore {
mock := &MockUnstructuredStore{ctrl: ctrl}
mock.recorder = &MockUnstructuredStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUnstructuredStore) EXPECT() *MockUnstructuredStoreMockRecorder {
return m.recorder
}
// ByID mocks base method.
func (m *MockUnstructuredStore) ByID(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ByID", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ByID indicates an expected call of ByID.
func (mr *MockUnstructuredStoreMockRecorder) ByID(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ByID", reflect.TypeOf((*MockUnstructuredStore)(nil).ByID), arg0, arg1, arg2)
}
// Create mocks base method.
func (m *MockUnstructuredStore) Create(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.APIObject) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Create indicates an expected call of Create.
func (mr *MockUnstructuredStoreMockRecorder) Create(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUnstructuredStore)(nil).Create), arg0, arg1, arg2)
}
// Delete mocks base method.
func (m *MockUnstructuredStore) Delete(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Delete indicates an expected call of Delete.
func (mr *MockUnstructuredStoreMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUnstructuredStore)(nil).Delete), arg0, arg1, arg2)
}
// ListByPartitions mocks base method.
func (m *MockUnstructuredStore) ListByPartitions(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 []partition.Partition) ([]unstructured.Unstructured, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByPartitions", arg0, arg1, arg2)
ret0, _ := ret[0].([]unstructured.Unstructured)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByPartitions indicates an expected call of ListByPartitions.
func (mr *MockUnstructuredStoreMockRecorder) ListByPartitions(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByPartitions", reflect.TypeOf((*MockUnstructuredStore)(nil).ListByPartitions), arg0, arg1, arg2)
}
// Update mocks base method.
func (m *MockUnstructuredStore) Update(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.APIObject, arg3 string) (*unstructured.Unstructured, []types.Warning, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].([]types.Warning)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Update indicates an expected call of Update.
func (mr *MockUnstructuredStoreMockRecorder) Update(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUnstructuredStore)(nil).Update), arg0, arg1, arg2, arg3)
}
// WatchByPartitions mocks base method.
func (m *MockUnstructuredStore) WatchByPartitions(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 types.WatchRequest, arg3 []partition.Partition) (chan watch.Event, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WatchByPartitions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(chan watch.Event)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WatchByPartitions indicates an expected call of WatchByPartitions.
func (mr *MockUnstructuredStoreMockRecorder) WatchByPartitions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchByPartitions", reflect.TypeOf((*MockUnstructuredStore)(nil).WatchByPartitions), arg0, arg1, arg2, arg3)
}

View File

@ -0,0 +1,118 @@
package sqlpartition
import (
"fmt"
"sort"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/wrangler/v2/pkg/kv"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
)
var (
passthroughPartitions = []partition.Partition{
{Passthrough: true},
}
)
// UnstructuredStore is like types.Store but deals in k8s unstructured objects instead of apiserver types.
// This interface exists in order for store to be mocked in tests
type UnstructuredStore interface {
ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, []types.Warning, error)
Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error)
Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error)
ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, string, error)
WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error)
}
// rbacPartitioner is an implementation of the sqlpartition.Partitioner interface.
type rbacPartitioner struct {
proxyStore UnstructuredStore
}
// All returns a slice of partitions applicable to the API schema and the user's access level.
// For watching individual resources or for blanket access permissions, it returns the passthrough partition.
// For more granular permissions, it returns a slice of partitions matching an allowed namespace or resource names.
func (p *rbacPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]partition.Partition, error) {
switch verb {
case "list":
fallthrough
case "watch":
if id != "" {
ns, name := kv.RSplit(id, "/")
return []partition.Partition{
{
Namespace: ns,
All: false,
Passthrough: false,
Names: sets.New[string](name),
},
}, nil
}
partitions, passthrough := isPassthrough(apiOp, schema, verb)
if passthrough {
return passthroughPartitions, nil
}
sort.Slice(partitions, func(i, j int) bool {
return partitions[i].Namespace < partitions[j].Namespace
})
return partitions, nil
default:
return nil, fmt.Errorf("parition all: invalid verb %s", verb)
}
}
// Store returns an Store suited to listing and watching resources by partition.
func (p *rbacPartitioner) Store() UnstructuredStore {
return p.proxyStore
}
// isPassthrough 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 isPassthrough(apiOp *types.APIRequest, schema *types.APISchema, verb string) ([]partition.Partition, bool) {
accessListByVerb, _ := attributes.Access(schema).(accesscontrol.AccessListByVerb)
if accessListByVerb.All(verb) {
return nil, true
}
resources := accessListByVerb.Granted(verb)
if apiOp.Namespace != "" {
if resources[apiOp.Namespace].All {
return nil, true
}
return []partition.Partition{
{
Namespace: apiOp.Namespace,
Names: sets.Set[string](resources[apiOp.Namespace].Names),
},
}, false
}
var result []partition.Partition
if attributes.Namespaced(schema) {
for k, v := range resources {
result = append(result, partition.Partition{
Namespace: k,
All: v.All,
Names: sets.Set[string](v.Names),
})
}
} else {
for _, v := range resources {
result = append(result, partition.Partition{
All: v.All,
Names: sets.Set[string](v.Names),
})
}
}
return result, false
}

View File

@ -0,0 +1,263 @@
package sqlpartition
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestAll(t *testing.T) {
tests := []struct {
name string
apiOp *types.APIRequest
id string
schema *types.APISchema
wantPartitions []partition.Partition
}{
{
name: "all passthrough",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "*",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: passthroughPartitions,
},
{
name: "global access for global request",
apiOp: &types.APIRequest{},
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
Attributes: map[string]interface{}{
"access": accesscontrol.AccessListByVerb{
"list": accesscontrol.AccessList{
accesscontrol.Access{
Namespace: "*",
ResourceName: "r1",
},
accesscontrol.Access{
Namespace: "*",
ResourceName: "r2",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Names: sets.New[string]("r1", "r2"),
},
},
},
{
name: "namespace access for global request",
apiOp: &types.APIRequest{},
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: "*",
},
accesscontrol.Access{
Namespace: "n2",
ResourceName: "*",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
All: true,
},
{
Namespace: "n2",
All: true,
},
},
},
{
name: "namespace access for namespaced request",
apiOp: &types.APIRequest{
Namespace: "n1",
},
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: passthroughPartitions,
},
{
// we still get a partition even if there is no access to it, it will be rejected by the API server later
name: "namespace access for invalid namespaced request",
apiOp: &types.APIRequest{
Namespace: "n2",
},
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: "n2",
},
},
},
{
name: "by names access for global request",
apiOp: &types.APIRequest{},
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",
},
accesscontrol.Access{
Namespace: "n2",
ResourceName: "r1",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1", "r2"),
},
{
Namespace: "n2",
Names: sets.New[string]("r1"),
},
},
},
{
name: "by names access for namespaced request",
apiOp: &types.APIRequest{
Namespace: "n1",
},
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: "r1",
},
},
},
},
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1"),
},
},
},
{
name: "by id",
apiOp: &types.APIRequest{},
id: "n1/r1",
schema: &types.APISchema{
Schema: &schemas.Schema{
ID: "foo",
},
},
wantPartitions: []partition.Partition{
{
Namespace: "n1",
Names: sets.New[string]("r1"),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
partitioner := rbacPartitioner{}
verb := "list"
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) {
expectedStore := NewMockUnstructuredStore(gomock.NewController(t))
rp := rbacPartitioner{
proxyStore: expectedStore,
}
store := rp.Store()
assert.Equal(t, expectedStore, store)
}

View File

@ -0,0 +1,142 @@
// Package sqlpartition implements a store which converts a request to partitions based on the user's rbac for
// the resource. For example, a user may request all items of resource A, but only have permissions for resource A in
// namespaces x,y,z. The partitions will then store that information and be passed to the next store.
package sqlpartition
import (
"context"
"github.com/rancher/apiserver/pkg/types"
lassopartition "github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/stores/partition"
)
// Partitioner is an interface for interacting with partitions.
type Partitioner interface {
All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]lassopartition.Partition, error)
Store() UnstructuredStore
}
type SchemaColumnSetter interface {
SetColumns(ctx context.Context, schema *types.APISchema) error
}
// Store implements types.proxyStore for partitions.
type Store struct {
Partitioner Partitioner
asl accesscontrol.AccessSetLookup
}
// NewStore creates a types.proxyStore implementation with a partitioner
func NewStore(store UnstructuredStore, asl accesscontrol.AccessSetLookup) *Store {
s := &Store{
Partitioner: &rbacPartitioner{
proxyStore: store,
},
asl: asl,
}
return s
}
// Delete deletes an object from a store.
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Delete(apiOp, schema, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// ByID looks up a single object by its ID.
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.ByID(apiOp, schema, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// List returns a list of objects across all applicable partitions.
// If pagination parameters are used, it returns a segment of the list.
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
var (
result types.APIObjectList
)
partitions, err := s.Partitioner.All(apiOp, schema, "list", "")
if err != nil {
return result, err
}
store := s.Partitioner.Store()
list, continueToken, err := store.ListByPartitions(apiOp, schema, partitions)
if err != nil {
return result, err
}
result.Count = len(list)
for _, item := range list {
item := item.DeepCopy()
result.Objects = append(result.Objects, partition.ToAPI(schema, item, nil))
}
result.Revision = ""
result.Continue = continueToken
return result, nil
}
// Create creates a single object in the store.
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Create(apiOp, schema, data)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// Update updates a single object in the store.
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
target := s.Partitioner.Store()
obj, warnings, err := target.Update(apiOp, schema, data, id)
if err != nil {
return types.APIObject{}, err
}
return partition.ToAPI(schema, obj, warnings), nil
}
// Watch returns a channel of events for a list or resource.
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
partitions, err := s.Partitioner.All(apiOp, schema, "watch", wr.ID)
if err != nil {
return nil, err
}
store := s.Partitioner.Store()
response := make(chan types.APIEvent)
c, err := store.WatchByPartitions(apiOp, schema, wr, partitions)
if err != nil {
return nil, err
}
go func() {
defer close(response)
for i := range c {
response <- partition.ToAPIEvent(nil, schema, i)
}
}()
return response, nil
}

View File

@ -0,0 +1,383 @@
package sqlpartition
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"github.com/golang/mock/gomock"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/assert"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/lasso/pkg/cache/sql/partition"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/stores/sqlproxy"
"github.com/rancher/wrangler/v2/pkg/generic"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
)
//go:generate mockgen --build_flags=--mod=mod -package sqlpartition -destination partition_mocks_test.go "github.com/rancher/steve/pkg/stores/sqlpartition" Partitioner,UnstructuredStore
func TestList(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "List() with no errors returned should returned no errors. Should have empty reivsion, count " +
"should match number of items in list, and id should include namespace (if applicable) and name, separated" +
" by a '/'.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
us := NewMockUnstructuredStore(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
partitions := make([]partition.Partition, 0)
uListToReturn := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "fruitsnamespace",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
expectedAPIObjList := types.APIObjectList{
Count: 1,
Revision: "",
Objects: []types.APIObject{
{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
"namespace": "fruitsnamespace",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
ID: "fruitsnamespace/fuji",
},
},
}
p.EXPECT().All(req, schema, "list", "").Return(partitions, nil)
p.EXPECT().Store().Return(us)
us.EXPECT().ListByPartitions(req, schema, partitions).Return(uListToReturn, "", nil)
l, err := s.List(req, schema)
assert.Nil(t, err)
assert.Equal(t, expectedAPIObjList, l)
},
})
tests = append(tests, testCase{
description: "List() with partitioner All() error returned should returned an error.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
p.EXPECT().All(req, schema, "list", "").Return(nil, fmt.Errorf("error"))
_, err := s.List(req, schema)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "List() with unstructured store ListByPartitions() error returned should returned an error.",
test: func(t *testing.T) {
p := NewMockPartitioner(gomock.NewController(t))
us := NewMockUnstructuredStore(gomock.NewController(t))
s := Store{
Partitioner: p,
}
req := &types.APIRequest{}
schema := &types.APISchema{
Schema: &schemas.Schema{},
}
partitions := make([]partition.Partition, 0)
p.EXPECT().All(req, schema, "list", "").Return(partitions, nil)
p.EXPECT().Store().Return(us)
us.EXPECT().ListByPartitions(req, schema, partitions).Return(nil, "", fmt.Errorf("error"))
_, err := s.List(req, schema)
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
type mockPartitioner struct {
store sqlproxy.Store
partitions map[string][]partition.Partition
}
func (m mockPartitioner) Lookup(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) (partition.Partition, error) {
panic("not implemented")
}
func (m mockPartitioner) All(apiOp *types.APIRequest, schema *types.APISchema, verb, id string) ([]partition.Partition, error) {
user, _ := request.UserFrom(apiOp.Request.Context())
return m.partitions[user.GetName()], nil
}
func (m mockPartitioner) Store() sqlproxy.Store {
return m.store
}
type mockStore struct {
contents map[string]*unstructured.UnstructuredList
partition partition.Partition
called map[string]int
}
func (m *mockStore) WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error) {
//TODO implement me
panic("implement me")
}
func (m *mockStore) ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, string, string, error) {
list := []unstructured.Unstructured{}
revision := ""
for _, partition := range partitions {
apiOp = apiOp.Clone()
apiOp.Namespace = partition.Namespace
partial, _, err := m.List(apiOp, schema)
if err != nil {
return nil, "", "", err
}
list = append(list, partial.Items...)
revision = partial.GetResourceVersion()
}
return list, revision, "", nil
}
func (m *mockStore) List(apiOp *types.APIRequest, schema *types.APISchema) (*unstructured.UnstructuredList, []types.Warning, error) {
n := apiOp.Namespace
previous, ok := m.called[n]
if !ok {
m.called[n] = 1
} else {
m.called[n] = previous + 1
}
query, _ := url.ParseQuery(apiOp.Request.URL.RawQuery)
l := query.Get("limit")
if l == "" {
return m.contents[n], nil, nil
}
i := 0
if c := query.Get("continue"); c != "" {
start, _ := base64.StdEncoding.DecodeString(c)
for j, obj := range m.contents[n].Items {
if string(start) == obj.GetName() {
i = j
break
}
}
}
lInt, _ := strconv.Atoi(l)
contents := m.contents[n].DeepCopy()
if len(contents.Items) > i+lInt {
contents.SetContinue(base64.StdEncoding.EncodeToString([]byte(contents.Items[i+lInt].GetName())))
}
if i > len(contents.Items) {
return contents, nil, nil
}
if i+lInt > len(contents.Items) {
contents.Items = contents.Items[i:]
return contents, nil, nil
}
contents.Items = contents.Items[i : i+lInt]
return contents, nil, nil
}
func (m *mockStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
panic("not implemented")
}
func (m *mockStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan watch.Event, error) {
panic("not implemented")
}
var colorMap = map[string]string{
"fuji": "pink",
"honeycrisp": "pink",
"granny-smith": "green",
"bramley": "green",
"crispin": "yellow",
"golden-delicious": "yellow",
"red-delicious": "red",
}
func newRequest(query, username string) *types.APIRequest {
return &types.APIRequest{
Request: (&http.Request{
URL: &url.URL{
Scheme: "https",
Host: "rancher",
Path: "/apples",
RawQuery: query,
},
}).WithContext(request.WithUser(context.Background(), &user.DefaultInfo{
Name: username,
Groups: []string{"system:authenticated"},
})),
}
}
type apple struct {
unstructured.Unstructured
}
func newApple(name string) apple {
return apple{unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": name,
},
"data": map[string]interface{}{
"color": colorMap[name],
},
},
}}
}
func (a apple) toObj() types.APIObject {
meta := a.Object["metadata"].(map[string]interface{})
id := meta["name"].(string)
ns, ok := meta["namespace"]
if ok {
id = ns.(string) + "/" + id
}
return types.APIObject{
Type: "apple",
ID: id,
Object: &a.Unstructured,
}
}
func (a apple) with(data map[string]string) apple {
for k, v := range data {
a.Object["data"].(map[string]interface{})[k] = v
}
return a
}
func (a apple) withNamespace(namespace string) apple {
a.Object["metadata"].(map[string]interface{})["namespace"] = namespace
return a
}
type mockAccessSetLookup struct {
accessID string
userRoles []map[string]string
}
func (m *mockAccessSetLookup) AccessFor(user user.Info) *accesscontrol.AccessSet {
userName := user.GetName()
access := getAccessID(userName, m.userRoles[0][userName])
m.userRoles = m.userRoles[1:]
return &accesscontrol.AccessSet{
ID: access,
}
}
func (m *mockAccessSetLookup) PurgeUserData(_ string) {
panic("not implemented")
}
func getAccessID(user, role string) string {
h := sha256.Sum256([]byte(user + role))
return string(h[:])
}
var namespaces = map[string]*corev1.Namespace{
"n1": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n1",
Labels: map[string]string{
"field.cattle.io/projectId": "p-abcde",
},
},
},
"n2": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n2",
Labels: map[string]string{
"field.cattle.io/projectId": "p-fghij",
},
},
},
"n3": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n3",
Labels: map[string]string{
"field.cattle.io/projectId": "p-klmno",
},
},
},
"n4": &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "n4",
},
},
}
type mockNamespaceCache struct{}
func (m mockNamespaceCache) Get(name string) (*corev1.Namespace, error) {
return namespaces[name], nil
}
func (m mockNamespaceCache) List(selector labels.Selector) ([]*corev1.Namespace, error) {
panic("not implemented")
}
func (m mockNamespaceCache) AddIndexer(indexName string, indexer generic.Indexer[*corev1.Namespace]) {
panic("not implemented")
}
func (m mockNamespaceCache) GetByIndex(indexName, key string) ([]*corev1.Namespace, error) {
panic("not implemented")
}

View File

@ -0,0 +1,232 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: k8s.io/client-go/dynamic (interfaces: ResourceInterface)
// Package sqlproxy is a generated GoMock package.
package sqlproxy
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
)
// MockResourceInterface is a mock of ResourceInterface interface.
type MockResourceInterface struct {
ctrl *gomock.Controller
recorder *MockResourceInterfaceMockRecorder
}
// MockResourceInterfaceMockRecorder is the mock recorder for MockResourceInterface.
type MockResourceInterfaceMockRecorder struct {
mock *MockResourceInterface
}
// NewMockResourceInterface creates a new mock instance.
func NewMockResourceInterface(ctrl *gomock.Controller) *MockResourceInterface {
mock := &MockResourceInterface{ctrl: ctrl}
mock.recorder = &MockResourceInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockResourceInterface) EXPECT() *MockResourceInterfaceMockRecorder {
return m.recorder
}
// Apply mocks base method.
func (m *MockResourceInterface) Apply(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions, arg4 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2, arg3}
for _, a := range arg4 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Apply", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Apply indicates an expected call of Apply.
func (mr *MockResourceInterfaceMockRecorder) Apply(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockResourceInterface)(nil).Apply), varargs...)
}
// ApplyStatus mocks base method.
func (m *MockResourceInterface) ApplyStatus(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplyStatus", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ApplyStatus indicates an expected call of ApplyStatus.
func (mr *MockResourceInterfaceMockRecorder) ApplyStatus(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyStatus", reflect.TypeOf((*MockResourceInterface)(nil).ApplyStatus), arg0, arg1, arg2, arg3)
}
// Create mocks base method.
func (m *MockResourceInterface) Create(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.CreateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Create", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockResourceInterfaceMockRecorder) Create(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockResourceInterface)(nil).Create), varargs...)
}
// Delete mocks base method.
func (m *MockResourceInterface) Delete(arg0 context.Context, arg1 string, arg2 v1.DeleteOptions, arg3 ...string) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockResourceInterfaceMockRecorder) Delete(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResourceInterface)(nil).Delete), varargs...)
}
// DeleteCollection mocks base method.
func (m *MockResourceInterface) DeleteCollection(arg0 context.Context, arg1 v1.DeleteOptions, arg2 v1.ListOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteCollection", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteCollection indicates an expected call of DeleteCollection.
func (mr *MockResourceInterfaceMockRecorder) DeleteCollection(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockResourceInterface)(nil).DeleteCollection), arg0, arg1, arg2)
}
// Get mocks base method.
func (m *MockResourceInterface) Get(arg0 context.Context, arg1 string, arg2 v1.GetOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockResourceInterfaceMockRecorder) Get(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockResourceInterface)(nil).Get), varargs...)
}
// List mocks base method.
func (m *MockResourceInterface) List(arg0 context.Context, arg1 v1.ListOptions) (*unstructured.UnstructuredList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockResourceInterfaceMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockResourceInterface)(nil).List), arg0, arg1)
}
// Patch mocks base method.
func (m *MockResourceInterface) Patch(arg0 context.Context, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 v1.PatchOptions, arg5 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2, arg3, arg4}
for _, a := range arg5 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Patch", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockResourceInterfaceMockRecorder) Patch(arg0, arg1, arg2, arg3, arg4 interface{}, arg5 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2, arg3, arg4}, arg5...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockResourceInterface)(nil).Patch), varargs...)
}
// Update mocks base method.
func (m *MockResourceInterface) Update(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Update", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockResourceInterfaceMockRecorder) Update(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockResourceInterface)(nil).Update), varargs...)
}
// UpdateStatus mocks base method.
func (m *MockResourceInterface) UpdateStatus(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateStatus", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateStatus indicates an expected call of UpdateStatus.
func (mr *MockResourceInterfaceMockRecorder) UpdateStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockResourceInterface)(nil).UpdateStatus), arg0, arg1, arg2)
}
// Watch mocks base method.
func (m *MockResourceInterface) Watch(arg0 context.Context, arg1 v1.ListOptions) (watch.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Watch", arg0, arg1)
ret0, _ := ret[0].(watch.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Watch indicates an expected call of Watch.
func (mr *MockResourceInterfaceMockRecorder) Watch(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockResourceInterface)(nil).Watch), arg0, arg1)
}

View File

@ -0,0 +1,359 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/steve/pkg/stores/sqlproxy (interfaces: Cache,ClientGetter,CacheFactory,SchemaColumnSetter,RelationshipNotifier)
// Package sqlproxy is a generated GoMock package.
package sqlproxy
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
types "github.com/rancher/apiserver/pkg/types"
informer "github.com/rancher/lasso/pkg/cache/sql/informer"
factory "github.com/rancher/lasso/pkg/cache/sql/informer/factory"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
summary "github.com/rancher/wrangler/v2/pkg/summary"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
schema "k8s.io/apimachinery/pkg/runtime/schema"
dynamic "k8s.io/client-go/dynamic"
kubernetes "k8s.io/client-go/kubernetes"
rest "k8s.io/client-go/rest"
)
// MockCache is a mock of Cache interface.
type MockCache struct {
ctrl *gomock.Controller
recorder *MockCacheMockRecorder
}
// MockCacheMockRecorder is the mock recorder for MockCache.
type MockCacheMockRecorder struct {
mock *MockCache
}
// NewMockCache creates a new mock instance.
func NewMockCache(ctrl *gomock.Controller) *MockCache {
mock := &MockCache{ctrl: ctrl}
mock.recorder = &MockCacheMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCache) EXPECT() *MockCacheMockRecorder {
return m.recorder
}
// ListByOptions mocks base method.
func (m *MockCache) ListByOptions(arg0 context.Context, arg1 informer.ListOptions, arg2 []partition.Partition, arg3 string) (*unstructured.UnstructuredList, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByOptions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByOptions indicates an expected call of ListByOptions.
func (mr *MockCacheMockRecorder) ListByOptions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOptions", reflect.TypeOf((*MockCache)(nil).ListByOptions), arg0, arg1, arg2, arg3)
}
// MockClientGetter is a mock of ClientGetter interface.
type MockClientGetter struct {
ctrl *gomock.Controller
recorder *MockClientGetterMockRecorder
}
// MockClientGetterMockRecorder is the mock recorder for MockClientGetter.
type MockClientGetterMockRecorder struct {
mock *MockClientGetter
}
// NewMockClientGetter creates a new mock instance.
func NewMockClientGetter(ctrl *gomock.Controller) *MockClientGetter {
mock := &MockClientGetter{ctrl: ctrl}
mock.recorder = &MockClientGetterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClientGetter) EXPECT() *MockClientGetterMockRecorder {
return m.recorder
}
// AdminClient mocks base method.
func (m *MockClientGetter) AdminClient(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AdminClient", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AdminClient indicates an expected call of AdminClient.
func (mr *MockClientGetterMockRecorder) AdminClient(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminClient", reflect.TypeOf((*MockClientGetter)(nil).AdminClient), arg0, arg1, arg2, arg3)
}
// AdminK8sInterface mocks base method.
func (m *MockClientGetter) AdminK8sInterface() (kubernetes.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AdminK8sInterface")
ret0, _ := ret[0].(kubernetes.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AdminK8sInterface indicates an expected call of AdminK8sInterface.
func (mr *MockClientGetterMockRecorder) AdminK8sInterface() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdminK8sInterface", reflect.TypeOf((*MockClientGetter)(nil).AdminK8sInterface))
}
// Client mocks base method.
func (m *MockClientGetter) Client(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Client indicates an expected call of Client.
func (mr *MockClientGetterMockRecorder) Client(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockClientGetter)(nil).Client), arg0, arg1, arg2, arg3)
}
// DynamicClient mocks base method.
func (m *MockClientGetter) DynamicClient(arg0 *types.APIRequest, arg1 rest.WarningHandler) (dynamic.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DynamicClient", arg0, arg1)
ret0, _ := ret[0].(dynamic.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DynamicClient indicates an expected call of DynamicClient.
func (mr *MockClientGetterMockRecorder) DynamicClient(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DynamicClient", reflect.TypeOf((*MockClientGetter)(nil).DynamicClient), arg0, arg1)
}
// IsImpersonating mocks base method.
func (m *MockClientGetter) IsImpersonating() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsImpersonating")
ret0, _ := ret[0].(bool)
return ret0
}
// IsImpersonating indicates an expected call of IsImpersonating.
func (mr *MockClientGetterMockRecorder) IsImpersonating() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsImpersonating", reflect.TypeOf((*MockClientGetter)(nil).IsImpersonating))
}
// K8sInterface mocks base method.
func (m *MockClientGetter) K8sInterface(arg0 *types.APIRequest) (kubernetes.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "K8sInterface", arg0)
ret0, _ := ret[0].(kubernetes.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// K8sInterface indicates an expected call of K8sInterface.
func (mr *MockClientGetterMockRecorder) K8sInterface(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "K8sInterface", reflect.TypeOf((*MockClientGetter)(nil).K8sInterface), arg0)
}
// TableAdminClient mocks base method.
func (m *MockClientGetter) TableAdminClient(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TableAdminClient", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TableAdminClient indicates an expected call of TableAdminClient.
func (mr *MockClientGetterMockRecorder) TableAdminClient(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TableAdminClient", reflect.TypeOf((*MockClientGetter)(nil).TableAdminClient), arg0, arg1, arg2, arg3)
}
// TableAdminClientForWatch mocks base method.
func (m *MockClientGetter) TableAdminClientForWatch(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TableAdminClientForWatch", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TableAdminClientForWatch indicates an expected call of TableAdminClientForWatch.
func (mr *MockClientGetterMockRecorder) TableAdminClientForWatch(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TableAdminClientForWatch", reflect.TypeOf((*MockClientGetter)(nil).TableAdminClientForWatch), arg0, arg1, arg2, arg3)
}
// TableClient mocks base method.
func (m *MockClientGetter) TableClient(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TableClient", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TableClient indicates an expected call of TableClient.
func (mr *MockClientGetterMockRecorder) TableClient(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TableClient", reflect.TypeOf((*MockClientGetter)(nil).TableClient), arg0, arg1, arg2, arg3)
}
// TableClientForWatch mocks base method.
func (m *MockClientGetter) TableClientForWatch(arg0 *types.APIRequest, arg1 *types.APISchema, arg2 string, arg3 rest.WarningHandler) (dynamic.ResourceInterface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TableClientForWatch", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(dynamic.ResourceInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TableClientForWatch indicates an expected call of TableClientForWatch.
func (mr *MockClientGetterMockRecorder) TableClientForWatch(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TableClientForWatch", reflect.TypeOf((*MockClientGetter)(nil).TableClientForWatch), arg0, arg1, arg2, arg3)
}
// MockCacheFactory is a mock of CacheFactory interface.
type MockCacheFactory struct {
ctrl *gomock.Controller
recorder *MockCacheFactoryMockRecorder
}
// MockCacheFactoryMockRecorder is the mock recorder for MockCacheFactory.
type MockCacheFactoryMockRecorder struct {
mock *MockCacheFactory
}
// NewMockCacheFactory creates a new mock instance.
func NewMockCacheFactory(ctrl *gomock.Controller) *MockCacheFactory {
mock := &MockCacheFactory{ctrl: ctrl}
mock.recorder = &MockCacheFactoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCacheFactory) EXPECT() *MockCacheFactoryMockRecorder {
return m.recorder
}
// CacheFor mocks base method.
func (m *MockCacheFactory) CacheFor(arg0 [][]string, arg1 dynamic.ResourceInterface, arg2 schema.GroupVersionKind, arg3 bool) (factory.Cache, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CacheFor", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(factory.Cache)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CacheFor indicates an expected call of CacheFor.
func (mr *MockCacheFactoryMockRecorder) CacheFor(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CacheFor", reflect.TypeOf((*MockCacheFactory)(nil).CacheFor), arg0, arg1, arg2, arg3)
}
// Reset mocks base method.
func (m *MockCacheFactory) Reset() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Reset")
ret0, _ := ret[0].(error)
return ret0
}
// Reset indicates an expected call of Reset.
func (mr *MockCacheFactoryMockRecorder) Reset() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockCacheFactory)(nil).Reset))
}
// MockSchemaColumnSetter is a mock of SchemaColumnSetter interface.
type MockSchemaColumnSetter struct {
ctrl *gomock.Controller
recorder *MockSchemaColumnSetterMockRecorder
}
// MockSchemaColumnSetterMockRecorder is the mock recorder for MockSchemaColumnSetter.
type MockSchemaColumnSetterMockRecorder struct {
mock *MockSchemaColumnSetter
}
// NewMockSchemaColumnSetter creates a new mock instance.
func NewMockSchemaColumnSetter(ctrl *gomock.Controller) *MockSchemaColumnSetter {
mock := &MockSchemaColumnSetter{ctrl: ctrl}
mock.recorder = &MockSchemaColumnSetterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSchemaColumnSetter) EXPECT() *MockSchemaColumnSetterMockRecorder {
return m.recorder
}
// SetColumns mocks base method.
func (m *MockSchemaColumnSetter) SetColumns(arg0 context.Context, arg1 *types.APISchema) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetColumns", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetColumns indicates an expected call of SetColumns.
func (mr *MockSchemaColumnSetterMockRecorder) SetColumns(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetColumns", reflect.TypeOf((*MockSchemaColumnSetter)(nil).SetColumns), arg0, arg1)
}
// MockRelationshipNotifier is a mock of RelationshipNotifier interface.
type MockRelationshipNotifier struct {
ctrl *gomock.Controller
recorder *MockRelationshipNotifierMockRecorder
}
// MockRelationshipNotifierMockRecorder is the mock recorder for MockRelationshipNotifier.
type MockRelationshipNotifierMockRecorder struct {
mock *MockRelationshipNotifier
}
// NewMockRelationshipNotifier creates a new mock instance.
func NewMockRelationshipNotifier(ctrl *gomock.Controller) *MockRelationshipNotifier {
mock := &MockRelationshipNotifier{ctrl: ctrl}
mock.recorder = &MockRelationshipNotifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRelationshipNotifier) EXPECT() *MockRelationshipNotifierMockRecorder {
return m.recorder
}
// OnInboundRelationshipChange mocks base method.
func (m *MockRelationshipNotifier) OnInboundRelationshipChange(arg0 context.Context, arg1 *types.APISchema, arg2 string) <-chan *summary.Relationship {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OnInboundRelationshipChange", arg0, arg1, arg2)
ret0, _ := ret[0].(<-chan *summary.Relationship)
return ret0
}
// OnInboundRelationshipChange indicates an expected call of OnInboundRelationshipChange.
func (mr *MockRelationshipNotifierMockRecorder) OnInboundRelationshipChange(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnInboundRelationshipChange", reflect.TypeOf((*MockRelationshipNotifier)(nil).OnInboundRelationshipChange), arg0, arg1, arg2)
}

View File

@ -0,0 +1,693 @@
// Package sqlproxy implements the proxy store, which is responsible for either interfacing directly with the Kubernetes API,
// or in the case of List, interfacing with an on-disk cache of items in the Kubernetes API.
package sqlproxy
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"sync"
"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/v2/pkg/data"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/rancher/wrangler/v2/pkg/schemas/validation"
"github.com/rancher/wrangler/v2/pkg/summary"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
apitypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
const watchTimeoutEnv = "CATTLE_WATCH_TIMEOUT_SECONDS"
var (
paramScheme = runtime.NewScheme()
paramCodec = runtime.NewParameterCodec(paramScheme)
typeSpecificIndexedFields = map[string][][]string{
"_v1_Namespace": {{`metadata`, `labels[field.cattle.io/projectId]`}},
"_v1_Node": {{`status`, `nodeInfo`, `kubeletVersion`}, {`status`, `nodeInfo`, `operatingSystem`}},
"_v1_Pod": {{`spec`, `containers`, `image`}, {`spec`, `nodeName`}},
"_v1_ConfigMap": {{`metadata`, `labels[harvesterhci.io/cloud-init-template]`}},
"management.cattle.io_v3_Node": {{`status`, `nodeName`}},
}
baseNSSchema = types.APISchema{
Schema: &schemas.Schema{
Attributes: map[string]interface{}{
"group": "",
"version": "v1",
"kind": "Namespace",
"resource": "namespaces",
},
},
}
)
func init() {
metav1.AddToGroupVersion(paramScheme, metav1.SchemeGroupVersion)
}
// ClientGetter is a dynamic kubernetes client factory.
type ClientGetter interface {
IsImpersonating() bool
K8sInterface(ctx *types.APIRequest) (kubernetes.Interface, error)
AdminK8sInterface() (kubernetes.Interface, error)
Client(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
DynamicClient(ctx *types.APIRequest, warningHandler rest.WarningHandler) (dynamic.Interface, error)
AdminClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
TableClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
TableAdminClient(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
TableClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
TableAdminClientForWatch(ctx *types.APIRequest, schema *types.APISchema, namespace string, warningHandler rest.WarningHandler) (dynamic.ResourceInterface, error)
}
type SchemaColumnSetter interface {
SetColumns(ctx context.Context, schema *types.APISchema) error
}
type Cache interface {
// ListByOptions returns objects according to the specified list options and partitions
// see ListOptionIndexer.ListByOptions
ListByOptions(ctx context.Context, lo informer.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, string, error)
}
// WarningBuffer holds warnings that may be returned from the kubernetes api
type WarningBuffer []types.Warning
// HandleWarningHeader takes the components of a kubernetes warning header and stores them
func (w *WarningBuffer) HandleWarningHeader(code int, agent string, text string) {
*w = append(*w, types.Warning{
Code: code,
Agent: agent,
Text: text,
})
}
// RelationshipNotifier is an interface for handling wrangler summary.Relationship events.
type RelationshipNotifier interface {
OnInboundRelationshipChange(ctx context.Context, schema *types.APISchema, namespace string) <-chan *summary.Relationship
}
type Store struct {
clientGetter ClientGetter
notifier RelationshipNotifier
cacheFactory CacheFactory
cfInitializer CacheFactoryInitializer
namespaceCache Cache
lock sync.Mutex
columnSetter SchemaColumnSetter
}
type CacheFactoryInitializer func() (CacheFactory, error)
type CacheFactory interface {
CacheFor(fields [][]string, client dynamic.ResourceInterface, gvk schema.GroupVersionKind, namespaced bool) (factory.Cache, error)
Reset() error
}
// NewProxyStore returns a Store implemented directly on top of kubernetes.
func NewProxyStore(c SchemaColumnSetter, clientGetter ClientGetter, notifier RelationshipNotifier, factory CacheFactory) (*Store, error) {
store := &Store{
clientGetter: clientGetter,
notifier: notifier,
columnSetter: c,
}
if factory == nil {
var err error
factory, err = defaultInitializeCacheFactory()
if err != nil {
return nil, err
}
}
store.cacheFactory = factory
if err := store.initializeNamespaceCache(); err != nil {
logrus.Infof("failed to warm up namespace informer for proxy store in steve, will try again on next ns request")
}
return store, nil
}
// Reset locks the store, resets the underlying cache factory, and warm the namespace cache.
func (s *Store) Reset() error {
s.lock.Lock()
defer s.lock.Unlock()
if err := s.cacheFactory.Reset(); err != nil {
return err
}
if err := s.initializeNamespaceCache(); err != nil {
return err
}
return nil
}
func defaultInitializeCacheFactory() (CacheFactory, error) {
informerFactory, err := factory.NewCacheFactory()
if err != nil {
return nil, err
}
return informerFactory, nil
}
// initializeNamespaceCache warms up the namespace cache as it is needed to process queries using options related to
// namespaces and projects.
func (s *Store) initializeNamespaceCache() error {
buffer := WarningBuffer{}
nsSchema := baseNSSchema
// make sure any relevant columns are set to the ns schema
if err := s.columnSetter.SetColumns(context.Background(), &nsSchema); err != nil {
return fmt.Errorf("failed to set columns for proxy stores namespace informer: %w", err)
}
// build table client
client, err := s.clientGetter.TableAdminClient(nil, &nsSchema, "", &buffer)
if err != nil {
return err
}
// get fields from schema's columns
fields := getFieldsFromSchema(&nsSchema)
// get any type-specific fields that steve is interested in
fields = append(fields, getFieldForGVK(attributes.GVK(&nsSchema))...)
// get the ns informer
nsInformer, err := s.cacheFactory.CacheFor(fields, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(&nsSchema), false)
if err != nil {
return err
}
s.namespaceCache = nsInformer
return nil
}
func getFieldForGVK(gvk schema.GroupVersionKind) [][]string {
return typeSpecificIndexedFields[keyFromGVK(gvk)]
}
func keyFromGVK(gvk schema.GroupVersionKind) string {
return gvk.Group + "_" + gvk.Version + "_" + gvk.Kind
}
// getFieldsFromSchema converts object field names from types.APISchema's format into lasso's
// cache.sql.informer's slice format (e.g. "metadata.resourceVersion" is ["metadata", "resourceVersion"])
func getFieldsFromSchema(schema *types.APISchema) [][]string {
var fields [][]string
columns := attributes.Columns(schema)
if columns == nil {
return nil
}
colDefs, ok := columns.([]common.ColumnDefinition)
if !ok {
return nil
}
for _, colDef := range colDefs {
field := strings.TrimPrefix(colDef.Field, "$.")
fields = append(fields, strings.Split(field, "."))
}
return fields
}
// ByID looks up a single object by its ID.
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
return s.byID(apiOp, schema, apiOp.Namespace, id)
}
func decodeParams(apiOp *types.APIRequest, target runtime.Object) error {
return paramCodec.DecodeParameters(apiOp.Request.URL.Query(), metav1.SchemeGroupVersion, target)
}
func (s *Store) byID(apiOp *types.APIRequest, schema *types.APISchema, namespace, id string) (*unstructured.Unstructured, []types.Warning, error) {
buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, namespace, &buffer))
if err != nil {
return nil, nil, err
}
opts := metav1.GetOptions{}
if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err
}
obj, err := k8sClient.Get(apiOp, id, opts)
rowToObject(obj)
return obj, buffer, err
}
func moveFromUnderscore(obj map[string]interface{}) map[string]interface{} {
if obj == nil {
return nil
}
for k := range types.ReservedFields {
v, ok := obj["_"+k]
delete(obj, "_"+k)
delete(obj, k)
if ok {
obj[k] = v
}
}
return obj
}
func rowToObject(obj *unstructured.Unstructured) {
if obj == nil {
return
}
if obj.Object["kind"] != "Table" ||
(obj.Object["apiVersion"] != "meta.k8s.io/v1" &&
obj.Object["apiVersion"] != "meta.k8s.io/v1beta1") {
return
}
items := tableToObjects(obj.Object)
if len(items) == 1 {
obj.Object = items[0].Object
}
}
func tableToList(obj *unstructured.UnstructuredList) {
if obj.Object["kind"] != "Table" ||
(obj.Object["apiVersion"] != "meta.k8s.io/v1" &&
obj.Object["apiVersion"] != "meta.k8s.io/v1beta1") {
return
}
obj.Items = tableToObjects(obj.Object)
}
func tableToObjects(obj map[string]interface{}) []unstructured.Unstructured {
var result []unstructured.Unstructured
rows, _ := obj["rows"].([]interface{})
for _, row := range rows {
m, ok := row.(map[string]interface{})
if !ok {
continue
}
cells := m["cells"]
object, ok := m["object"].(map[string]interface{})
if !ok {
continue
}
data.PutValue(object, cells, "metadata", "fields")
result = append(result, unstructured.Unstructured{
Object: object,
})
}
return result
}
func returnErr(err error, c chan watch.Event) {
c <- watch.Event{
Type: watch.Error,
Object: &metav1.Status{
Message: err.Error(),
},
}
}
func (s *Store) listAndWatch(apiOp *types.APIRequest, client dynamic.ResourceInterface, schema *types.APISchema, w types.WatchRequest, result chan watch.Event) {
rev := w.Revision
if rev == "-1" || rev == "0" {
rev = ""
}
timeout := int64(60 * 30)
timeoutSetting := os.Getenv(watchTimeoutEnv)
if timeoutSetting != "" {
userSetTimeout, err := strconv.Atoi(timeoutSetting)
if err != nil {
logrus.Debugf("could not parse %s environment variable, error: %v", watchTimeoutEnv, err)
} else {
timeout = int64(userSetTimeout)
}
}
k8sClient, _ := metricsStore.Wrap(client, nil)
watcher, err := k8sClient.Watch(apiOp, metav1.ListOptions{
Watch: true,
TimeoutSeconds: &timeout,
ResourceVersion: rev,
LabelSelector: w.Selector,
})
if err != nil {
returnErr(errors.Wrapf(err, "stopping watch for %s: %v", schema.ID, err), result)
return
}
defer watcher.Stop()
logrus.Debugf("opening watcher for %s", schema.ID)
eg, ctx := errgroup.WithContext(apiOp.Context())
go func() {
<-ctx.Done()
watcher.Stop()
}()
if s.notifier != nil {
eg.Go(func() error {
for rel := range s.notifier.OnInboundRelationshipChange(ctx, schema, apiOp.Namespace) {
obj, _, err := s.byID(apiOp, schema, rel.Namespace, rel.Name)
if err == nil {
rowToObject(obj)
result <- watch.Event{Type: watch.Modified, Object: obj}
} else {
returnErr(errors.Wrapf(err, "notifier watch error: %v", err), result)
}
}
return fmt.Errorf("closed")
})
}
eg.Go(func() error {
for event := range watcher.ResultChan() {
if event.Type == watch.Error {
if status, ok := event.Object.(*metav1.Status); ok {
returnErr(fmt.Errorf("event watch error: %s", status.Message), result)
} else {
logrus.Debugf("event watch error: could not decode event object %T", event.Object)
}
continue
}
if unstr, ok := event.Object.(*unstructured.Unstructured); ok {
rowToObject(unstr)
}
result <- event
}
return fmt.Errorf("closed")
})
_ = eg.Wait()
return
}
// WatchNames returns a channel of events filtered by an allowed set of names.
// In plain kubernetes, if a user has permission to 'list' or 'watch' a defined set of resource names,
// performing the list or watch will result in a Forbidden error, because the user does not have permission
// to list *all* resources.
// With this filter, the request can be performed successfully, and only the allowed resources will
// be returned in watch.
func (s *Store) WatchNames(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, names sets.Set[string]) (chan watch.Event, error) {
buffer := &WarningBuffer{}
adminClient, err := s.clientGetter.TableAdminClientForWatch(apiOp, schema, apiOp.Namespace, buffer)
if err != nil {
return nil, err
}
c, err := s.watch(apiOp, schema, w, adminClient)
if err != nil {
return nil, err
}
result := make(chan watch.Event)
go func() {
defer close(result)
for item := range c {
if item.Type == watch.Error {
if status, ok := item.Object.(*metav1.Status); ok {
logrus.Debugf("WatchNames received error: %s", status.Message)
} else {
logrus.Debugf("WatchNames received error: %v", item)
}
result <- item
continue
}
m, err := meta.Accessor(item.Object)
if err != nil {
logrus.Debugf("WatchNames cannot process unexpected object: %s", err)
continue
}
if names.Has(m.GetName()) {
result <- item
}
}
}()
return result, nil
}
// Watch returns a channel of events for a list or resource.
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan watch.Event, error) {
buffer := &WarningBuffer{}
client, err := s.clientGetter.TableClientForWatch(apiOp, schema, apiOp.Namespace, buffer)
if err != nil {
return nil, err
}
return s.watch(apiOp, schema, w, client)
}
func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest, client dynamic.ResourceInterface) (chan watch.Event, error) {
result := make(chan watch.Event)
go func() {
s.listAndWatch(apiOp, client, schema, w, result)
logrus.Debugf("closing watcher for %s", schema.ID)
close(result)
}()
return result, nil
}
// Create creates a single object in the store.
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject) (*unstructured.Unstructured, []types.Warning, error) {
var (
resp *unstructured.Unstructured
)
input := params.Data()
if input == nil {
input = data.Object{}
}
name := types.Name(input)
ns := types.Namespace(input)
if name == "" && input.String("metadata", "generateName") == "" {
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
}
if ns == "" && apiOp.Namespace != "" {
ns = apiOp.Namespace
input.SetNested(ns, "metadata", "namespace")
}
gvk := attributes.GVK(schema)
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer))
if err != nil {
return nil, nil, err
}
opts := metav1.CreateOptions{}
if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err
}
resp, err = k8sClient.Create(apiOp, &unstructured.Unstructured{Object: input}, opts)
rowToObject(resp)
return resp, buffer, err
}
// Update updates a single object in the store.
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject, id string) (*unstructured.Unstructured, []types.Warning, error) {
var (
err error
input = params.Data()
)
ns := types.Namespace(input)
buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, ns, &buffer))
if err != nil {
return nil, nil, err
}
if apiOp.Method == http.MethodPatch {
bytes, err := ioutil.ReadAll(io.LimitReader(apiOp.Request.Body, 2<<20))
if err != nil {
return nil, nil, err
}
pType := apitypes.StrategicMergePatchType
if apiOp.Request.Header.Get("content-type") == string(apitypes.JSONPatchType) {
pType = apitypes.JSONPatchType
}
opts := metav1.PatchOptions{}
if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err
}
if pType == apitypes.StrategicMergePatchType {
data := map[string]interface{}{}
if err := json.Unmarshal(bytes, &data); err != nil {
return nil, nil, err
}
data = moveFromUnderscore(data)
bytes, err = json.Marshal(data)
if err != nil {
return nil, nil, err
}
}
resp, err := k8sClient.Patch(apiOp, id, pType, bytes, opts)
if err != nil {
return nil, nil, err
}
return resp, buffer, nil
}
resourceVersion := input.String("metadata", "resourceVersion")
if resourceVersion == "" {
return nil, nil, fmt.Errorf("metadata.resourceVersion is required for update")
}
opts := metav1.UpdateOptions{}
if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, err
}
resp, err := k8sClient.Update(apiOp, &unstructured.Unstructured{Object: moveFromUnderscore(input)}, metav1.UpdateOptions{})
if err != nil {
return nil, nil, err
}
rowToObject(resp)
return resp, buffer, nil
}
// Delete deletes an object from a store.
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, []types.Warning, error) {
opts := metav1.DeleteOptions{}
if err := decodeParams(apiOp, &opts); err != nil {
return nil, nil, nil
}
buffer := WarningBuffer{}
k8sClient, err := metricsStore.Wrap(s.clientGetter.TableClient(apiOp, schema, apiOp.Namespace, &buffer))
if err != nil {
return nil, nil, err
}
if err := k8sClient.Delete(apiOp, id, opts); err != nil {
return nil, nil, err
}
obj, _, err := s.byID(apiOp, schema, apiOp.Namespace, id)
if err != nil {
// ignore lookup error
return nil, nil, validation.ErrorCode{
Status: http.StatusNoContent,
}
}
return obj, buffer, nil
}
// ListByPartitions returns an unstructured list of resources belonging to any of the specified partitions
func (s *Store) ListByPartitions(apiOp *types.APIRequest, schema *types.APISchema, partitions []partition.Partition) ([]unstructured.Unstructured, string, error) {
opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache)
if err != nil {
return nil, "", err
}
// warnings from inside the informer are discarded
buffer := WarningBuffer{}
client, err := s.clientGetter.TableAdminClient(apiOp, schema, "", &buffer)
if err != nil {
return nil, "", err
}
fields := getFieldsFromSchema(schema)
fields = append(fields, getFieldForGVK(attributes.GVK(schema))...)
inf, err := s.cacheFactory.CacheFor(fields, &tablelistconvert.Client{ResourceInterface: client}, attributes.GVK(schema), attributes.Namespaced(schema))
if err != nil {
return nil, "", err
}
list, continueToken, err := inf.ListByOptions(apiOp.Context(), opts, partitions, apiOp.Namespace)
if err != nil {
if errors.Is(err, informer.InvalidColumnErr) {
return nil, "", apierror.NewAPIError(validation.InvalidBodyContent, err.Error())
}
return nil, "", err
}
return list.Items, continueToken, nil
}
// WatchByPartitions returns a channel of events for a list or resource belonging to any of the specified partitions
func (s *Store) WatchByPartitions(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest, partitions []partition.Partition) (chan watch.Event, error) {
ctx, cancel := context.WithCancel(apiOp.Context())
apiOp = apiOp.Clone().WithContext(ctx)
eg := errgroup.Group{}
result := make(chan watch.Event)
for _, partition := range partitions {
p := partition
eg.Go(func() error {
defer cancel()
c, err := s.watchByPartition(p, apiOp, schema, wr)
if err != nil {
return err
}
for i := range c {
result <- i
}
return nil
})
}
go func() {
defer close(result)
<-ctx.Done()
eg.Wait()
cancel()
}()
return result, nil
}
// watchByPartition returns a channel of events for a list or resource belonging to a specified partition
func (s *Store) watchByPartition(partition partition.Partition, apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan watch.Event, error) {
if partition.Passthrough {
return s.Watch(apiOp, schema, wr)
}
apiOp.Namespace = partition.Namespace
if partition.All {
return s.Watch(apiOp, schema, wr)
}
return s.WatchNames(apiOp, schema, wr, partition.Names)
}

View File

@ -0,0 +1,718 @@
package sqlproxy
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"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"
"github.com/rancher/steve/pkg/stores/sqlpartition/listprocessor"
"github.com/rancher/steve/pkg/stores/sqlproxy/tablelistconvert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/pkg/errors"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/client"
"github.com/rancher/wrangler/v2/pkg/schemas"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
clientgotesting "k8s.io/client-go/testing"
)
//go:generate mockgen --build_flags=--mod=mod -package sqlproxy -destination ./proxy_mocks_test.go github.com/rancher/steve/pkg/stores/sqlproxy Cache,ClientGetter,CacheFactory,SchemaColumnSetter,RelationshipNotifier
//go:generate mockgen --build_flags=--mod=mod -package sqlproxy -destination ./sql_informer_mocks_test.go github.com/rancher/lasso/pkg/cache/sql/informer ByOptionsLister
//go:generate mockgen --build_flags=--mod=mod -package sqlproxy -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
var c *watch.FakeWatcher
type testFactory struct {
*client.Factory
fakeClient *fake.FakeDynamicClient
}
func TestNewProxyStore(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "NewProxyStore() with no errors returned should returned no errors. Should initialize and assign" +
" a namespace cache.",
test: func(t *testing.T) {
scc := NewMockSchemaColumnSetter(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
rn := NewMockRelationshipNotifier(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
bloi := NewMockByOptionsLister(gomock.NewController(t))
c := factory.Cache{
ByOptionsLister: &informer.Informer{
ByOptionsLister: bloi,
},
}
nsSchema := baseNSSchema
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
cf.EXPECT().CacheFor([][]string{{"metadata", "labels[field.cattle.io/projectId]"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false).Return(c, nil)
s, err := NewProxyStore(scc, cg, rn, cf)
assert.Nil(t, err)
assert.Equal(t, scc, s.columnSetter)
assert.Equal(t, cg, s.clientGetter)
assert.Equal(t, rn, s.notifier)
assert.Equal(t, s.cacheFactory, cf)
assert.NotNil(t, s.namespaceCache)
},
})
tests = append(tests, testCase{
description: "NewProxyStore() with schema column setter SetColumns() error returned should return not return and error" +
" and not set namespace cache.",
test: func(t *testing.T) {
scc := NewMockSchemaColumnSetter(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
rn := NewMockRelationshipNotifier(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
nsSchema := baseNSSchema
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(fmt.Errorf("error"))
s, err := NewProxyStore(scc, cg, rn, cf)
assert.Nil(t, err)
assert.Equal(t, scc, s.columnSetter)
assert.Equal(t, cg, s.clientGetter)
assert.Equal(t, rn, s.notifier)
assert.Equal(t, s.cacheFactory, cf)
assert.Nil(t, s.namespaceCache)
},
})
tests = append(tests, testCase{
description: "NewProxyStore() with client getter TableAdminClient() error returned should return not return and error" +
" and not set namespace cache.",
test: func(t *testing.T) {
scc := NewMockSchemaColumnSetter(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
rn := NewMockRelationshipNotifier(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
nsSchema := baseNSSchema
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(nil, fmt.Errorf("error"))
s, err := NewProxyStore(scc, cg, rn, cf)
assert.Nil(t, err)
assert.Equal(t, scc, s.columnSetter)
assert.Equal(t, cg, s.clientGetter)
assert.Equal(t, rn, s.notifier)
assert.Equal(t, s.cacheFactory, cf)
assert.Nil(t, s.namespaceCache)
},
})
tests = append(tests, testCase{
description: "NewProxyStore() with client getter TableAdminClient() error returned should return not return and error" +
" and not set namespace cache.",
test: func(t *testing.T) {
scc := NewMockSchemaColumnSetter(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
rn := NewMockRelationshipNotifier(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
nsSchema := baseNSSchema
scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
cf.EXPECT().CacheFor([][]string{{"metadata", "labels[field.cattle.io/projectId]"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false).Return(factory.Cache{}, fmt.Errorf("error"))
s, err := NewProxyStore(scc, cg, rn, cf)
assert.Nil(t, err)
assert.Equal(t, scc, s.columnSetter)
assert.Equal(t, cg, s.clientGetter)
assert.Equal(t, rn, s.notifier)
assert.Equal(t, s.cacheFactory, cf)
assert.Nil(t, s.namespaceCache)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
func TestListByPartitions(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "client ListByPartitions() with no errors returned should returned no errors. Should pass fields" +
" from schema.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
bloi := NewMockByOptionsLister(gomock.NewController(t))
inf := &informer.Informer{
ByOptionsLister: bloi,
}
c := factory.Cache{
ByOptionsLister: inf,
}
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map
cf.EXPECT().CacheFor([][]string{{"some", "field"}, {"gvk", "specific", "fields"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema)).Return(c, nil)
bloi.EXPECT().ListByOptions(req.Context(), opts, partitions, req.Namespace).Return(listToReturn, "", nil)
list, contToken, err := s.ListByPartitions(req, schema, partitions)
assert.Nil(t, err)
assert.Equal(t, expectedItems, list)
assert.Equal(t, "", contToken)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with ParseQuery error returned should return an error.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{RawQuery: "projectsornamespaces=somethin"},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
nsi.EXPECT().ListByOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", fmt.Errorf("error")).Times(2)
_, err := listprocessor.ParseQuery(req, nsi)
assert.NotNil(t, err)
_, _, err = s.ListByPartitions(req, schema, partitions)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with no errors returned should returned no errors. Should pass fields" +
" from schema.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(nil, fmt.Errorf("error"))
_, _, err = s.ListByPartitions(req, schema, partitions)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with CacheFor() error returned should returned an errors. Should pass fields",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
_, err := listprocessor.ParseQuery(req, nil)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map
cf.EXPECT().CacheFor([][]string{{"some", "field"}, {"gvk", "specific", "fields"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema)).Return(factory.Cache{}, fmt.Errorf("error"))
_, _, err = s.ListByPartitions(req, schema, partitions)
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client ListByPartitions() with ListByOptions() error returned should return an errors. Should pass fields" +
" from schema.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
bloi := NewMockByOptionsLister(gomock.NewController(t))
inf := &informer.Informer{
ByOptionsLister: bloi,
}
c := factory.Cache{
ByOptionsLister: inf,
}
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
}
var partitions []partition.Partition
req := &types.APIRequest{
Request: &http.Request{
URL: &url.URL{},
},
}
schema := &types.APISchema{
Schema: &schemas.Schema{Attributes: map[string]interface{}{
"columns": []common.ColumnDefinition{
{
Field: "some.field",
},
},
}},
}
expectedItems := []unstructured.Unstructured{
{
Object: map[string]interface{}{
"kind": "apple",
"metadata": map[string]interface{}{
"name": "fuji",
},
"data": map[string]interface{}{
"color": "pink",
},
},
},
}
listToReturn := &unstructured.UnstructuredList{
Items: make([]unstructured.Unstructured, len(expectedItems), len(expectedItems)),
}
gvk := schema2.GroupVersionKind{
Group: "some",
Version: "test",
Kind: "gvk",
}
typeSpecificIndexedFields["some_test_gvk"] = [][]string{{"gvk", "specific", "fields"}}
attributes.SetGVK(schema, gvk)
// ListByPartitions copies point so we need some original record of items to ensure as asserting listToReturn's
// items is equal to the list returned by ListByParititons doesn't ensure no mutation happened
copy(listToReturn.Items, expectedItems)
opts, err := listprocessor.ParseQuery(req, nil)
assert.Nil(t, err)
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
// This tests that fields are being extracted from schema columns and the type specific fields map
cf.EXPECT().CacheFor([][]string{{"some", "field"}, {"gvk", "specific", "fields"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema)).Return(c, nil)
bloi.EXPECT().ListByOptions(req.Context(), opts, partitions, req.Namespace).Return(nil, "", fmt.Errorf("error"))
_, _, err = s.ListByPartitions(req, schema, partitions)
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
func TestReset(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "client Reset() with no errors returned should returned no errors.",
test: func(t *testing.T) {
nsc := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
cs := NewMockSchemaColumnSetter(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
nsc2 := factory.Cache{}
s := &Store{
namespaceCache: nsc,
clientGetter: cg,
cacheFactory: cf,
columnSetter: cs,
cfInitializer: func() (CacheFactory, error) { return cf, nil },
}
nsSchema := baseNSSchema
cf.EXPECT().Reset().Return(nil)
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
cf.EXPECT().CacheFor([][]string{{"metadata", "labels[field.cattle.io/projectId]"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false).Return(nsc2, nil)
err := s.Reset()
assert.Nil(t, err)
assert.Equal(t, nsc2, s.namespaceCache)
},
})
tests = append(tests, testCase{
description: "client Reset() with cache factory Reset() error returned, should return an error.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
cs := NewMockSchemaColumnSetter(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
columnSetter: cs,
cfInitializer: func() (CacheFactory, error) { return cf, nil },
}
cf.EXPECT().Reset().Return(fmt.Errorf("error"))
err := s.Reset()
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client Reset() with column setter error returned, should return an error.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
cs := NewMockSchemaColumnSetter(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
columnSetter: cs,
cfInitializer: func() (CacheFactory, error) { return cf, nil },
}
cf.EXPECT().Reset().Return(nil)
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(fmt.Errorf("error"))
err := s.Reset()
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client Reset() with column getter TableAdminClient() error returned, should return an error.",
test: func(t *testing.T) {
nsi := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
cs := NewMockSchemaColumnSetter(gomock.NewController(t))
s := &Store{
namespaceCache: nsi,
clientGetter: cg,
cacheFactory: cf,
columnSetter: cs,
cfInitializer: func() (CacheFactory, error) { return cf, nil },
}
nsSchema := baseNSSchema
cf.EXPECT().Reset().Return(nil)
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(nil, fmt.Errorf("error"))
err := s.Reset()
assert.NotNil(t, err)
},
})
tests = append(tests, testCase{
description: "client Reset() with cache factory CacheFor() error returned, should return an error.",
test: func(t *testing.T) {
nsc := NewMockCache(gomock.NewController(t))
cg := NewMockClientGetter(gomock.NewController(t))
cf := NewMockCacheFactory(gomock.NewController(t))
cs := NewMockSchemaColumnSetter(gomock.NewController(t))
ri := NewMockResourceInterface(gomock.NewController(t))
s := &Store{
namespaceCache: nsc,
clientGetter: cg,
cacheFactory: cf,
columnSetter: cs,
cfInitializer: func() (CacheFactory, error) { return cf, nil },
}
nsSchema := baseNSSchema
cf.EXPECT().Reset().Return(nil)
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
cf.EXPECT().CacheFor([][]string{{"metadata", "labels[field.cattle.io/projectId]"}}, &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false).Return(factory.Cache{}, fmt.Errorf("error"))
err := s.Reset()
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
func TestWatchNamesErrReceive(t *testing.T) {
testClientFactory, err := client.NewFactory(&rest.Config{}, false)
assert.Nil(t, err)
fakeClient := fake.NewSimpleDynamicClient(runtime.NewScheme())
c = watch.NewFakeWithChanSize(5, true)
defer c.Stop()
errMsgsToSend := []string{"err1", "err2", "err3"}
c.Add(&v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "testsecret1"}})
for index := range errMsgsToSend {
c.Error(&metav1.Status{
Message: errMsgsToSend[index],
})
}
c.Add(&v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "testsecret2"}})
fakeClient.PrependWatchReactor("*", func(action clientgotesting.Action) (handled bool, ret watch.Interface, err error) {
return true, c, nil
})
testStore := Store{
clientGetter: &testFactory{Factory: testClientFactory,
fakeClient: fakeClient,
},
}
apiSchema := &types.APISchema{Schema: &schemas.Schema{Attributes: map[string]interface{}{"table": "something"}}}
wc, err := testStore.WatchNames(&types.APIRequest{Namespace: "", Schema: apiSchema, Request: &http.Request{}}, apiSchema, types.WatchRequest{}, sets.New[string]("testsecret1", "testsecret2"))
assert.Nil(t, err)
eg := errgroup.Group{}
eg.Go(func() error { return receiveUntil(wc, 5*time.Second) })
err = eg.Wait()
assert.Nil(t, err)
assert.Equal(t, 0, len(c.ResultChan()), "Expected all secrets to have been received")
}
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 {
timer := time.NewTicker(d)
defer timer.Stop()
secretNames := []string{"testsecret1", "testsecret2"}
errMsgs := []string{"err1", "err2", "err3"}
for {
select {
case event, ok := <-wc:
if !ok {
return errors.New("watch chan should not have been closed")
}
if event.Type == watch.Error {
status, ok := event.Object.(*metav1.Status)
if !ok {
continue
}
if strings.HasSuffix(status.Message, errMsgs[0]) {
errMsgs = errMsgs[1:]
}
}
secret, ok := event.Object.(*v1.Secret)
if !ok {
continue
}
if secret.Name == secretNames[0] {
secretNames = secretNames[1:]
}
if len(secretNames) == 0 && len(errMsgs) == 0 {
return nil
}
continue
case <-timer.C:
return errors.New("timed out waiting to receiving objects from chan")
}
}
}

View File

@ -0,0 +1,54 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/rancher/lasso/pkg/cache/sql/informer (interfaces: ByOptionsLister)
// Package sqlproxy is a generated GoMock package.
package sqlproxy
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
informer "github.com/rancher/lasso/pkg/cache/sql/informer"
partition "github.com/rancher/lasso/pkg/cache/sql/partition"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MockByOptionsLister is a mock of ByOptionsLister interface.
type MockByOptionsLister struct {
ctrl *gomock.Controller
recorder *MockByOptionsListerMockRecorder
}
// MockByOptionsListerMockRecorder is the mock recorder for MockByOptionsLister.
type MockByOptionsListerMockRecorder struct {
mock *MockByOptionsLister
}
// NewMockByOptionsLister creates a new mock instance.
func NewMockByOptionsLister(ctrl *gomock.Controller) *MockByOptionsLister {
mock := &MockByOptionsLister{ctrl: ctrl}
mock.recorder = &MockByOptionsListerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockByOptionsLister) EXPECT() *MockByOptionsListerMockRecorder {
return m.recorder
}
// ListByOptions mocks base method.
func (m *MockByOptionsLister) ListByOptions(arg0 context.Context, arg1 informer.ListOptions, arg2 []partition.Partition, arg3 string) (*unstructured.UnstructuredList, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListByOptions", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListByOptions indicates an expected call of ListByOptions.
func (mr *MockByOptionsListerMockRecorder) ListByOptions(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByOptions", reflect.TypeOf((*MockByOptionsLister)(nil).ListByOptions), arg0, arg1, arg2, arg3)
}

View File

@ -0,0 +1,133 @@
/*
Package tablelistconvert provides a client that will use a table client but convert *UnstructuredList and *Unstructured objects
returned by ByID and List to resemble those returned by non-table clients while preserving some table-related data.
*/
package tablelistconvert
import (
"context"
"fmt"
"github.com/rancher/wrangler/v2/pkg/data"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8sWatch "k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
)
type Client struct {
dynamic.ResourceInterface
}
var _ dynamic.ResourceInterface = (*Client)(nil)
type tableConvertWatch struct {
done chan struct{}
events chan k8sWatch.Event
k8sWatch.Interface
}
// List will return an *UnstructuredList that contains Items instead of just using the Object field to store a table as
// Table Clients do. The items will preserve values for columns in the form of metadata.fields.
func (c *Client) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
list, err := c.ResourceInterface.List(ctx, opts)
if err != nil {
return nil, err
}
tableToList(list)
return list, nil
}
func (c *Client) Watch(ctx context.Context, opts metav1.ListOptions) (k8sWatch.Interface, error) {
w, err := c.ResourceInterface.Watch(ctx, opts)
if err != nil {
return nil, err
}
events := make(chan k8sWatch.Event)
done := make(chan struct{})
eventWatch := &tableConvertWatch{done: done, events: events, Interface: w}
eventWatch.feed()
return eventWatch, nil
}
func (w *tableConvertWatch) feed() {
tableEvents := w.Interface.ResultChan()
go func() {
for {
select {
case e, ok := <-tableEvents:
if !ok {
close(w.events)
return
}
if unstr, ok := e.Object.(*unstructured.Unstructured); ok {
rowToObject(unstr)
w.events <- e
}
case <-w.done:
close(w.events)
return
}
}
}()
}
func (w *tableConvertWatch) ResultChan() <-chan k8sWatch.Event {
return w.events
}
func (w *tableConvertWatch) Stop() {
fmt.Println("stop")
close(w.done)
w.Interface.Stop()
}
func rowToObject(obj *unstructured.Unstructured) {
if obj == nil {
return
}
if obj.Object["kind"] != "Table" ||
(obj.Object["apiVersion"] != "meta.k8s.io/v1" &&
obj.Object["apiVersion"] != "meta.k8s.io/v1beta1") {
return
}
items := tableToObjects(obj.Object)
if len(items) == 1 {
obj.Object = items[0].Object
}
}
func tableToList(obj *unstructured.UnstructuredList) {
if obj.Object["kind"] != "Table" ||
(obj.Object["apiVersion"] != "meta.k8s.io/v1" &&
obj.Object["apiVersion"] != "meta.k8s.io/v1beta1") {
return
}
obj.Items = tableToObjects(obj.Object)
}
func tableToObjects(obj map[string]interface{}) []unstructured.Unstructured {
var result []unstructured.Unstructured
rows, _ := obj["rows"].([]interface{})
for _, row := range rows {
m, ok := row.(map[string]interface{})
if !ok {
continue
}
cells := m["cells"]
object, ok := m["object"].(map[string]interface{})
if !ok {
continue
}
data.PutValue(object, cells, "metadata", "fields")
result = append(result, unstructured.Unstructured{
Object: object,
})
}
return result
}

View File

@ -0,0 +1,270 @@
package tablelistconvert
import (
"context"
"fmt"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
watch2 "k8s.io/apimachinery/pkg/watch"
"testing"
"time"
)
//go:generate mockgen --build_flags=--mod=mod -package tablelistconvert -destination ./dynamic_mocks_test.go k8s.io/client-go/dynamic ResourceInterface
//go:generate mockgen --build_flags=--mod=mod -package tablelistconvert -destination ./watch_mocks_test.go k8s.io/apimachinery/pkg/watch Interface
func TestWatch(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "client Watch() with no errors returned should returned no errors. Objects passed to underlying channel should" +
" be sent with expected metadata.fields",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
watch := NewMockInterface(gomock.NewController(t))
testEvents := make(chan watch2.Event)
opts := metav1.ListOptions{}
ri.EXPECT().Watch(context.TODO(), opts).Return(watch, nil)
initialEvent := watch2.Event{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Table",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{},
},
},
},
},
}
expectedEvent := watch2.Event{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"fields": []interface{}{"cell1", "cell2"},
},
},
},
}
go func() {
time.Sleep(1 * time.Second)
testEvents <- initialEvent
}()
watch.EXPECT().ResultChan().Return(testEvents)
client := &Client{ResourceInterface: ri}
receivedWatch, err := client.Watch(context.TODO(), opts)
assert.Nil(t, err)
receivedEvent, ok := <-receivedWatch.ResultChan()
assert.True(t, ok)
assert.Equal(t, expectedEvent, receivedEvent)
},
})
tests = append(tests, testCase{
description: "client Watch() with no errors returned should returned no errors. Objects passed to underlying channel that are not of type \"table\"" +
" should not be sent with metadata.fields",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
watch := NewMockInterface(gomock.NewController(t))
testEvents := make(chan watch2.Event)
opts := metav1.ListOptions{}
ri.EXPECT().Watch(context.TODO(), opts).Return(watch, nil)
initialEvent := watch2.Event{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "NotTable",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{},
},
},
},
},
}
expectedEvent := watch2.Event{
Object: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "NotTable",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{},
},
},
},
},
}
go func() {
time.Sleep(1 * time.Second)
testEvents <- initialEvent
}()
watch.EXPECT().ResultChan().Return(testEvents)
client := &Client{ResourceInterface: ri}
receivedWatch, err := client.Watch(context.TODO(), opts)
assert.Nil(t, err)
receivedEvent, ok := <-receivedWatch.ResultChan()
assert.True(t, ok)
assert.Equal(t, expectedEvent, receivedEvent)
},
})
tests = append(tests, testCase{
description: "client Watch() with no errors returned should returned no errors. Nil objects passed to underlying" +
" channel should be sent as nil",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
watch := NewMockInterface(gomock.NewController(t))
testEvents := make(chan watch2.Event)
opts := metav1.ListOptions{}
ri.EXPECT().Watch(context.TODO(), opts).Return(watch, nil)
initialEvent := watch2.Event{
Object: &unstructured.Unstructured{
nil,
},
}
expectedEvent := watch2.Event{
Object: &unstructured.Unstructured{
Object: nil,
},
}
go func() {
time.Sleep(1 * time.Second)
testEvents <- initialEvent
}()
watch.EXPECT().ResultChan().Return(testEvents)
client := &Client{ResourceInterface: ri}
receivedWatch, err := client.Watch(context.TODO(), opts)
assert.Nil(t, err)
receivedEvent, ok := <-receivedWatch.ResultChan()
assert.True(t, ok)
assert.Equal(t, expectedEvent, receivedEvent)
},
})
tests = append(tests, testCase{
description: "client Watch() with error returned from resource client should returned an errors",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
opts := metav1.ListOptions{}
ri.EXPECT().Watch(context.TODO(), opts).Return(nil, fmt.Errorf("error"))
client := &Client{ResourceInterface: ri}
_, err := client.Watch(context.TODO(), opts)
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}
func TestList(t *testing.T) {
type testCase struct {
description string
test func(t *testing.T)
}
var tests []testCase
tests = append(tests, testCase{
description: "client List() with no errors returned should returned no errors. Received list should be mutated" +
"to contain rows.objects in Objects field and metadata.fields should be added to both.",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
opts := metav1.ListOptions{}
initialList := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"kind": "Table",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{},
},
},
},
Items: []unstructured.Unstructured{
{},
},
}
expectedList := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"kind": "Table",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{
"metadata": map[string]interface{}{
"fields": []interface{}{"cell1", "cell2"},
}},
},
},
},
Items: []unstructured.Unstructured{
{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"fields": []interface{}{"cell1", "cell2"},
},
}},
},
}
ri.EXPECT().List(context.TODO(), opts).Return(initialList, nil)
client := &Client{ResourceInterface: ri}
receivedList, err := client.List(context.TODO(), opts)
assert.Nil(t, err)
assert.Equal(t, expectedList, receivedList)
},
})
tests = append(tests, testCase{
description: "client List() with no errors returned should returned no errors. Received list should be not mutated" +
"if kind is not \"Table\".",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
opts := metav1.ListOptions{}
initialList := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"kind": "NotTable",
"apiVersion": "meta.k8s.io/v1",
"rows": []interface{}{
map[string]interface{}{
"cells": []interface{}{"cell1", "cell2"},
"object": map[string]interface{}{},
},
},
},
Items: []unstructured.Unstructured{
{},
},
}
ri.EXPECT().List(context.TODO(), opts).Return(initialList, nil)
client := &Client{ResourceInterface: ri}
receivedList, err := client.List(context.TODO(), opts)
assert.Nil(t, err)
assert.Equal(t, initialList, receivedList)
},
})
tests = append(tests, testCase{
description: "client List() with errors returned from Resource Interface should returned an error" +
"if kind is not \"Table\".",
test: func(t *testing.T) {
ri := NewMockResourceInterface(gomock.NewController(t))
opts := metav1.ListOptions{}
ri.EXPECT().List(context.TODO(), opts).Return(nil, fmt.Errorf("error"))
client := &Client{ResourceInterface: ri}
_, err := client.List(context.TODO(), opts)
assert.NotNil(t, err)
},
})
t.Parallel()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) { test.test(t) })
}
}

View File

@ -0,0 +1,232 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: k8s.io/client-go/dynamic (interfaces: ResourceInterface)
// Package tablelistconvert is a generated GoMock package.
package tablelistconvert
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
)
// MockResourceInterface is a mock of ResourceInterface interface.
type MockResourceInterface struct {
ctrl *gomock.Controller
recorder *MockResourceInterfaceMockRecorder
}
// MockResourceInterfaceMockRecorder is the mock recorder for MockResourceInterface.
type MockResourceInterfaceMockRecorder struct {
mock *MockResourceInterface
}
// NewMockResourceInterface creates a new mock instance.
func NewMockResourceInterface(ctrl *gomock.Controller) *MockResourceInterface {
mock := &MockResourceInterface{ctrl: ctrl}
mock.recorder = &MockResourceInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockResourceInterface) EXPECT() *MockResourceInterfaceMockRecorder {
return m.recorder
}
// Apply mocks base method.
func (m *MockResourceInterface) Apply(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions, arg4 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2, arg3}
for _, a := range arg4 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Apply", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Apply indicates an expected call of Apply.
func (mr *MockResourceInterfaceMockRecorder) Apply(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockResourceInterface)(nil).Apply), varargs...)
}
// ApplyStatus mocks base method.
func (m *MockResourceInterface) ApplyStatus(arg0 context.Context, arg1 string, arg2 *unstructured.Unstructured, arg3 v1.ApplyOptions) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplyStatus", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ApplyStatus indicates an expected call of ApplyStatus.
func (mr *MockResourceInterfaceMockRecorder) ApplyStatus(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyStatus", reflect.TypeOf((*MockResourceInterface)(nil).ApplyStatus), arg0, arg1, arg2, arg3)
}
// Create mocks base method.
func (m *MockResourceInterface) Create(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.CreateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Create", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockResourceInterfaceMockRecorder) Create(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockResourceInterface)(nil).Create), varargs...)
}
// Delete mocks base method.
func (m *MockResourceInterface) Delete(arg0 context.Context, arg1 string, arg2 v1.DeleteOptions, arg3 ...string) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockResourceInterfaceMockRecorder) Delete(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResourceInterface)(nil).Delete), varargs...)
}
// DeleteCollection mocks base method.
func (m *MockResourceInterface) DeleteCollection(arg0 context.Context, arg1 v1.DeleteOptions, arg2 v1.ListOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteCollection", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteCollection indicates an expected call of DeleteCollection.
func (mr *MockResourceInterfaceMockRecorder) DeleteCollection(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockResourceInterface)(nil).DeleteCollection), arg0, arg1, arg2)
}
// Get mocks base method.
func (m *MockResourceInterface) Get(arg0 context.Context, arg1 string, arg2 v1.GetOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockResourceInterfaceMockRecorder) Get(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockResourceInterface)(nil).Get), varargs...)
}
// List mocks base method.
func (m *MockResourceInterface) List(arg0 context.Context, arg1 v1.ListOptions) (*unstructured.UnstructuredList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", arg0, arg1)
ret0, _ := ret[0].(*unstructured.UnstructuredList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List.
func (mr *MockResourceInterfaceMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockResourceInterface)(nil).List), arg0, arg1)
}
// Patch mocks base method.
func (m *MockResourceInterface) Patch(arg0 context.Context, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 v1.PatchOptions, arg5 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2, arg3, arg4}
for _, a := range arg5 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Patch", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockResourceInterfaceMockRecorder) Patch(arg0, arg1, arg2, arg3, arg4 interface{}, arg5 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2, arg3, arg4}, arg5...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockResourceInterface)(nil).Patch), varargs...)
}
// Update mocks base method.
func (m *MockResourceInterface) Update(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions, arg3 ...string) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Update", varargs...)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockResourceInterfaceMockRecorder) Update(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockResourceInterface)(nil).Update), varargs...)
}
// UpdateStatus mocks base method.
func (m *MockResourceInterface) UpdateStatus(arg0 context.Context, arg1 *unstructured.Unstructured, arg2 v1.UpdateOptions) (*unstructured.Unstructured, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateStatus", arg0, arg1, arg2)
ret0, _ := ret[0].(*unstructured.Unstructured)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateStatus indicates an expected call of UpdateStatus.
func (mr *MockResourceInterfaceMockRecorder) UpdateStatus(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockResourceInterface)(nil).UpdateStatus), arg0, arg1, arg2)
}
// Watch mocks base method.
func (m *MockResourceInterface) Watch(arg0 context.Context, arg1 v1.ListOptions) (watch.Interface, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Watch", arg0, arg1)
ret0, _ := ret[0].(watch.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Watch indicates an expected call of Watch.
func (mr *MockResourceInterfaceMockRecorder) Watch(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockResourceInterface)(nil).Watch), arg0, arg1)
}

View File

@ -0,0 +1,61 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: k8s.io/apimachinery/pkg/watch (interfaces: Interface)
// Package tablelistconvert is a generated GoMock package.
package tablelistconvert
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
watch "k8s.io/apimachinery/pkg/watch"
)
// MockInterface is a mock of Interface interface.
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface.
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance.
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// ResultChan mocks base method.
func (m *MockInterface) ResultChan() <-chan watch.Event {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResultChan")
ret0, _ := ret[0].(<-chan watch.Event)
return ret0
}
// ResultChan indicates an expected call of ResultChan.
func (mr *MockInterfaceMockRecorder) ResultChan() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResultChan", reflect.TypeOf((*MockInterface)(nil).ResultChan))
}
// Stop mocks base method.
func (m *MockInterface) Stop() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Stop")
}
// Stop indicates an expected call of Stop.
func (mr *MockInterfaceMockRecorder) Stop() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockInterface)(nil).Stop))
}