diff --git a/go.mod b/go.mod index c08e43ee..02c495eb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f5cbd7a6..8e3916bb 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index eb81280c..94459292 100644 --- a/main.go +++ b/main.go @@ -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 } diff --git a/pkg/controllers/schema/schemas.go b/pkg/controllers/schema/schemas.go index 2bc09096..5bcf5d11 100644 --- a/pkg/controllers/schema/schemas.go +++ b/pkg/controllers/schema/schemas.go @@ -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{ diff --git a/pkg/resources/common/formatter.go b/pkg/resources/common/formatter.go index 53743919..5ed8ec84 100644 --- a/pkg/resources/common/formatter.go +++ b/pkg/resources/common/formatter.go @@ -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" { diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go index 96cdcb50..40a71c4f 100644 --- a/pkg/resources/schema.go +++ b/pkg/resources/schema.go @@ -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) + }, + }, + } +} diff --git a/pkg/server/cli/clicontext.go b/pkg/server/cli/clicontext.go index 0e40c7f0..ba31446f 100644 --- a/pkg/server/cli/clicontext.go +++ b/pkg/server/cli/clicontext.go @@ -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, }) } diff --git a/pkg/server/server.go b/pkg/server/server.go index 2097aea2..5cd96a8e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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 } diff --git a/pkg/stores/partition/store.go b/pkg/stores/partition/store.go index f42b65e6..b09d8656 100644 --- a/pkg/stores/partition/store.go +++ b/pkg/stores/partition/store.go @@ -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 { diff --git a/pkg/stores/proxy/error_wrapper.go b/pkg/stores/proxy/error_wrapper.go index a4515b37..deefe45c 100644 --- a/pkg/stores/proxy/error_wrapper.go +++ b/pkg/stores/proxy/error_wrapper.go @@ -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) } diff --git a/pkg/stores/proxy/proxy_store.go b/pkg/stores/proxy/proxy_store.go index 4df94c82..90545c45 100644 --- a/pkg/stores/proxy/proxy_store.go +++ b/pkg/stores/proxy/proxy_store.go @@ -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( diff --git a/pkg/stores/proxy/unformatter.go b/pkg/stores/proxy/unformatter.go index 8d5905ad..16da0d05 100644 --- a/pkg/stores/proxy/unformatter.go +++ b/pkg/stores/proxy/unformatter.go @@ -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) diff --git a/pkg/stores/proxy/watch_refresh.go b/pkg/stores/proxy/watch_refresh.go index b6e19296..f6e3368e 100644 --- a/pkg/stores/proxy/watch_refresh.go +++ b/pkg/stores/proxy/watch_refresh.go @@ -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()) diff --git a/pkg/stores/sqlpartition/listprocessor/processor.go b/pkg/stores/sqlpartition/listprocessor/processor.go new file mode 100644 index 00000000..bf7acb31 --- /dev/null +++ b/pkg/stores/sqlpartition/listprocessor/processor.go @@ -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 +} diff --git a/pkg/stores/sqlpartition/listprocessor/processor_test.go b/pkg/stores/sqlpartition/listprocessor/processor_test.go new file mode 100644 index 00000000..c65e7e62 --- /dev/null +++ b/pkg/stores/sqlpartition/listprocessor/processor_test.go @@ -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) + }) + } +} diff --git a/pkg/stores/sqlpartition/listprocessor/proxy_mocks_test.go b/pkg/stores/sqlpartition/listprocessor/proxy_mocks_test.go new file mode 100644 index 00000000..63e2c89e --- /dev/null +++ b/pkg/stores/sqlpartition/listprocessor/proxy_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlpartition/partition_mocks_test.go b/pkg/stores/sqlpartition/partition_mocks_test.go new file mode 100644 index 00000000..82eb846d --- /dev/null +++ b/pkg/stores/sqlpartition/partition_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlpartition/partitioner.go b/pkg/stores/sqlpartition/partitioner.go new file mode 100644 index 00000000..5de04340 --- /dev/null +++ b/pkg/stores/sqlpartition/partitioner.go @@ -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 +} diff --git a/pkg/stores/sqlpartition/partitioner_test.go b/pkg/stores/sqlpartition/partitioner_test.go new file mode 100644 index 00000000..0f1775af --- /dev/null +++ b/pkg/stores/sqlpartition/partitioner_test.go @@ -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) +} diff --git a/pkg/stores/sqlpartition/store.go b/pkg/stores/sqlpartition/store.go new file mode 100644 index 00000000..83c26214 --- /dev/null +++ b/pkg/stores/sqlpartition/store.go @@ -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 +} diff --git a/pkg/stores/sqlpartition/store_test.go b/pkg/stores/sqlpartition/store_test.go new file mode 100644 index 00000000..341a579b --- /dev/null +++ b/pkg/stores/sqlpartition/store_test.go @@ -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") +} diff --git a/pkg/stores/sqlproxy/dynamic_mocks_test.go b/pkg/stores/sqlproxy/dynamic_mocks_test.go new file mode 100644 index 00000000..38d37083 --- /dev/null +++ b/pkg/stores/sqlproxy/dynamic_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlproxy/proxy_mocks_test.go b/pkg/stores/sqlproxy/proxy_mocks_test.go new file mode 100644 index 00000000..2749ae00 --- /dev/null +++ b/pkg/stores/sqlproxy/proxy_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlproxy/proxy_store.go b/pkg/stores/sqlproxy/proxy_store.go new file mode 100644 index 00000000..9d25d3a3 --- /dev/null +++ b/pkg/stores/sqlproxy/proxy_store.go @@ -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) +} diff --git a/pkg/stores/sqlproxy/proxy_store_test.go b/pkg/stores/sqlproxy/proxy_store_test.go new file mode 100644 index 00000000..144892b8 --- /dev/null +++ b/pkg/stores/sqlproxy/proxy_store_test.go @@ -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") + } + } +} diff --git a/pkg/stores/sqlproxy/sql_informer_mocks_test.go b/pkg/stores/sqlproxy/sql_informer_mocks_test.go new file mode 100644 index 00000000..ffd68acd --- /dev/null +++ b/pkg/stores/sqlproxy/sql_informer_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlproxy/tablelistconvert/client.go b/pkg/stores/sqlproxy/tablelistconvert/client.go new file mode 100644 index 00000000..e76d307e --- /dev/null +++ b/pkg/stores/sqlproxy/tablelistconvert/client.go @@ -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 +} diff --git a/pkg/stores/sqlproxy/tablelistconvert/client_test.go b/pkg/stores/sqlproxy/tablelistconvert/client_test.go new file mode 100644 index 00000000..fcea2353 --- /dev/null +++ b/pkg/stores/sqlproxy/tablelistconvert/client_test.go @@ -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) }) + } +} diff --git a/pkg/stores/sqlproxy/tablelistconvert/dynamic_mocks_test.go b/pkg/stores/sqlproxy/tablelistconvert/dynamic_mocks_test.go new file mode 100644 index 00000000..6b2e7fa2 --- /dev/null +++ b/pkg/stores/sqlproxy/tablelistconvert/dynamic_mocks_test.go @@ -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) +} diff --git a/pkg/stores/sqlproxy/tablelistconvert/watch_mocks_test.go b/pkg/stores/sqlproxy/tablelistconvert/watch_mocks_test.go new file mode 100644 index 00000000..623c614f --- /dev/null +++ b/pkg/stores/sqlproxy/tablelistconvert/watch_mocks_test.go @@ -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)) +}